Skip to content

Commit

Permalink
prefer having the viewmodel implementation in the same file as the in…
Browse files Browse the repository at this point in the history
…terface

There is only ever one implementation of the interface. The only reason why an interface exists is so that the StateFlows exposed on the interface are not mutable
  • Loading branch information
westnordost committed Mar 26, 2024
1 parent ef6eb16 commit 1be40b8
Show file tree
Hide file tree
Showing 17 changed files with 525 additions and 523 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ import org.koin.dsl.module
val aboutScreenModule = module {
viewModel<LogsViewModel> { LogsViewModelImpl(get()) }
viewModel<CreditsViewModel> { CreditsViewModelImpl(get()) }
viewModel<ChangelogViewModel> { ChangelogViewModelImpl(get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import de.westnordost.streetcomplete.screens.HasTitle
import de.westnordost.streetcomplete.screens.TwoPaneDetailFragment
import de.westnordost.streetcomplete.util.ktx.getRawTextFile
import de.westnordost.streetcomplete.util.ktx.indicesOf
import de.westnordost.streetcomplete.util.ktx.observe
import de.westnordost.streetcomplete.util.ktx.pxToDp

Check failure on line 22 in app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Unused import
import de.westnordost.streetcomplete.util.ktx.pxToSp

Check failure on line 23 in app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Unused import
import de.westnordost.streetcomplete.util.ktx.setHtmlBody
Expand All @@ -28,21 +29,21 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.math.roundToInt

Check failure on line 33 in app/src/main/java/de/westnordost/streetcomplete/screens/about/ChangelogFragment.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Unused import

/** Shows the full changelog */
class ChangelogFragment : TwoPaneDetailFragment(R.layout.fragment_changelog), HasTitle {

private val binding by viewBinding(FragmentChangelogBinding::bind)
private val viewModel by viewModel<ChangelogViewModel>()

override val title: String get() = getString(R.string.about_title_changelog)

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

viewLifecycleScope.launch {
val changelog = readChangelog(resources)
binding.webView.setHtmlBody(changelog)
observe(viewModel.changelog) { changelog ->
if (changelog != null) binding.webView.setHtmlBody(changelog)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package de.westnordost.streetcomplete.screens.about

import android.content.res.Resources
import androidx.lifecycle.ViewModel
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.util.ktx.getRawTextFile
import de.westnordost.streetcomplete.util.ktx.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

abstract class ChangelogViewModel : ViewModel() {
abstract val changelog: StateFlow<String?>
}

class ChangelogViewModelImpl(resources: Resources) : ChangelogViewModel() {
override val changelog = MutableStateFlow<String?>(null)

init {
launch(Dispatchers.IO) { changelog.value = resources.getRawTextFile(R.raw.changelog) }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package de.westnordost.streetcomplete.screens.about

import android.content.res.Resources
import androidx.lifecycle.ViewModel
import de.westnordost.streetcomplete.R
import de.westnordost.streetcomplete.util.ktx.getYamlObject
import de.westnordost.streetcomplete.util.ktx.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable

Expand All @@ -27,3 +33,60 @@ data class Contributor(
) {
val githubLink: String get() = "https://github.com/$githubUsername"
}

class CreditsViewModelImpl(private val resources: Resources) : CreditsViewModel() {
override val credits = MutableStateFlow<Credits?>(null)

init {
launch(Dispatchers.IO) {
val mainContributors = readMainContributors()
credits.value = Credits(
mainContributors = mainContributors,
codeContributors = readCodeContributors(mainContributors.map { it.githubUsername }),
projectsContributors = readProjectsContributors(),
artContributors = readArtContributors(),
translators = readTranslators()
)
}
}

private fun readMainContributors(): List<Contributor> =
resources.getYamlObject<List<Contributor>>(R.raw.credits_main)

private fun readArtContributors(): List<String> =
resources.getYamlObject<List<String>>(R.raw.credits_art)

private fun readProjectsContributors(): List<String> =
resources.getYamlObject<List<String>>(R.raw.credits_projects)

private fun readCodeContributors(skipUsers: List<String?>): List<Contributor> {

Check failure on line 62 in app/src/main/java/de/westnordost/streetcomplete/screens/about/CreditsViewModel.kt

View workflow job for this annotation

GitHub Actions / Kotlin

Function body should be replaced with body expression
return resources
.getYamlObject<List<Contributor>>(R.raw.credits_contributors)
.filter { it.githubUsername !in skipUsers && it.score >= 50 }
.sortedByDescending { it.score }
}

private fun readTranslators(): Map<String, List<String>> {
val translatorsByLanguage =
resources.getYamlObject<MutableMap<String, MutableMap<String, Int>>>(R.raw.credits_translators)

// skip plain English. That's not a translation
translatorsByLanguage.remove("en")

// skip those translators who contributed less than 2% of the translation
for (contributors in translatorsByLanguage.values) {
val totalTranslated = contributors.values.sum()
val removedAnyone = contributors.values.removeAll { 100 * it / totalTranslated < 2 }
if (removedAnyone) {
contributors[""] = 1
}
}

return translatorsByLanguage.mapValues { (_, translators) ->
translators.entries.sortedByDescending { it.value }.map { it.key }
}
}
}

private val Contributor.score: Int get() =
linesOfCodeChanged + linesOfInterfaceMarkupChanged / 5 + assetFilesChanged * 15

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,13 +1,86 @@
package de.westnordost.streetcomplete.screens.about

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.westnordost.streetcomplete.data.logs.LogMessage
import de.westnordost.streetcomplete.data.logs.LogsController
import de.westnordost.streetcomplete.data.logs.LogsFilters
import de.westnordost.streetcomplete.util.ktx.systemTimeNow
import de.westnordost.streetcomplete.util.ktx.toEpochMilli
import de.westnordost.streetcomplete.util.ktx.toLocalDate
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.plus
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime

abstract class LogsViewModel : ViewModel() {
abstract val filters: StateFlow<LogsFilters>
abstract val logs: StateFlow<List<LogMessage>>

abstract fun setFilters(filters: LogsFilters)
}

class LogsViewModelImpl(
private val logsController: LogsController,
) : LogsViewModel() {

override val filters = MutableStateFlow(LogsFilters(
timestampNewerThan = LocalDateTime(systemTimeNow().toLocalDate(), LocalTime(0, 0, 0))
))

/**
* Produce a call back flow of all incoming logs matching the given [filters].
*/
private fun getIncomingLogs(filters: LogsFilters) = callbackFlow {
// Listener that sends the messages matching the filters to the observer
val listener = object : LogsController.Listener {
override fun onAdded(message: LogMessage) {
if (filters.matches(message)) {
trySend(message) // Send it to the observer
}
}
}
logsController.addListener(listener)
awaitClose { logsController.removeListener(listener) }
}

@OptIn(ExperimentalCoroutinesApi::class)
private val _logs: SharedFlow<List<LogMessage>> =
filters.transformLatest { filters ->
val logs = logsController.getLogs(filters).toMutableList()

emit(logs)

getIncomingLogs(filters).collect {
logs.add(it)
emit(logs)
}
}.shareIn(viewModelScope + Dispatchers.IO, SharingStarted.Eagerly, 1)

override val logs: StateFlow<List<LogMessage>> = object :
StateFlow<List<LogMessage>>,
SharedFlow<List<LogMessage>> by _logs {
override val value: List<LogMessage> get() = replayCache[0]
}

override fun setFilters(filters: LogsFilters) {
this.filters.value = filters
}
}

private fun LogsController.getLogs(filters: LogsFilters) =
getLogs(
levels = filters.levels,
messageContains = filters.messageContains,
newerThan = filters.timestampNewerThan?.toEpochMilli(),
olderThan = filters.timestampOlderThan?.toEpochMilli()
)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,28 @@ package de.westnordost.streetcomplete.screens.user.achievements

import androidx.lifecycle.ViewModel
import de.westnordost.streetcomplete.data.user.achievements.Achievement
import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource
import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource
import de.westnordost.streetcomplete.util.ktx.launch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

abstract class AchievementsViewModel : ViewModel() {
abstract val isSynchronizingStatistics: StateFlow<Boolean>
abstract val achievements: StateFlow<List<Pair<Achievement, Int>>?>
}

class AchievementsViewModelImpl(
private val achievementsSource: AchievementsSource,
private val statisticsSource: StatisticsSource,
) : AchievementsViewModel() {
override val isSynchronizingStatistics = MutableStateFlow(statisticsSource.isSynchronizing)
override val achievements = MutableStateFlow<List<Pair<Achievement, Int>>?>(null)

init {
launch(Dispatchers.IO) {
achievements.value = achievementsSource.getAchievements()
}
}
}
Loading

0 comments on commit 1be40b8

Please sign in to comment.