Skip to content

Commit

Permalink
Merge pull request #5482 from streetcomplete/viewmodels
Browse files Browse the repository at this point in the history
ViewModels (a start)
  • Loading branch information
westnordost committed Mar 8, 2024
2 parents 22b6179 + 14e9d2b commit ace1fbd
Show file tree
Hide file tree
Showing 31 changed files with 699 additions and 401 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ dependencies {
implementation("androidx.viewpager:viewpager:1.0.0")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")

implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
// photos
implementation("androidx.exifinterface:exifinterface:1.3.7")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.westnordost.streetcomplete.data.osm.edits

import de.westnordost.streetcomplete.data.AllEditTypes
import de.westnordost.streetcomplete.data.ApplicationDbTestCase
import de.westnordost.streetcomplete.data.osm.edits.create.CreateNodeAction
import de.westnordost.streetcomplete.data.osm.edits.create.RevertCreateNodeAction
Expand Down Expand Up @@ -43,7 +44,7 @@ class ElementEditsDaoTest : ApplicationDbTestCase() {
@BeforeTest fun createDao() {
val list = listOf(1 to TEST_QUEST_TYPE, 2 to TEST_QUEST_TYPE2)
val list2 = listOf(1 to TestOverlay)
dao = ElementEditsDao(database, QuestTypeRegistry(list), OverlayRegistry(list2))
dao = ElementEditsDao(database, AllEditTypes(listOf(QuestTypeRegistry(list), OverlayRegistry(list2))))
}

@Test fun addGet_UpdateElementTagsEdit() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import androidx.work.WorkManager
import de.westnordost.streetcomplete.data.CacheTrimmer
import de.westnordost.streetcomplete.data.CleanerWorker
import de.westnordost.streetcomplete.data.Preloader
import de.westnordost.streetcomplete.data.allEditTypesModule
import de.westnordost.streetcomplete.data.dbModule
import de.westnordost.streetcomplete.data.download.downloadModule
import de.westnordost.streetcomplete.data.download.tiles.DownloadedTilesController
Expand Down Expand Up @@ -39,11 +40,13 @@ import de.westnordost.streetcomplete.data.visiblequests.questPresetsModule
import de.westnordost.streetcomplete.overlays.overlaysModule
import de.westnordost.streetcomplete.quests.oneway_suspects.data.trafficFlowSegmentsModule
import de.westnordost.streetcomplete.quests.questsModule
import de.westnordost.streetcomplete.screens.about.aboutScreenModule
import de.westnordost.streetcomplete.screens.main.mainModule
import de.westnordost.streetcomplete.screens.main.map.mapModule
import de.westnordost.streetcomplete.screens.measure.arModule
import de.westnordost.streetcomplete.screens.settings.ResurveyIntervalsUpdater
import de.westnordost.streetcomplete.screens.settings.settingsModule
import de.westnordost.streetcomplete.screens.user.userScreenModule
import de.westnordost.streetcomplete.util.CrashReportExceptionHandler
import de.westnordost.streetcomplete.util.getDefaultTheme
import de.westnordost.streetcomplete.util.getSelectedLocale
Expand Down Expand Up @@ -92,6 +95,8 @@ class StreetCompleteApplication : Application() {
modules(
achievementsModule,
appModule,
aboutScreenModule,
userScreenModule,
createdElementsModule,
dbModule,
logsModule,
Expand All @@ -113,6 +118,7 @@ class StreetCompleteApplication : Application() {
preferencesModule,
questModule,
questPresetsModule,
allEditTypesModule,
questsModule,
settingsModule,
statisticsModule,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.westnordost.streetcomplete.data

import de.westnordost.streetcomplete.data.osm.edits.EditType

class AllEditTypes(
registries: List<ObjectTypeRegistry<out EditType>>
) : AbstractCollection<EditType>() {

private val byName = registries.flatten().associateByTo(LinkedHashMap()) { it.name }

override val size: Int get() = byName.size

override fun iterator(): Iterator<EditType> = byName.values.iterator()

fun getByName(typeName: String): EditType? = byName[typeName]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.westnordost.streetcomplete.data

import de.westnordost.streetcomplete.data.overlays.OverlayRegistry
import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry
import org.koin.dsl.module

val allEditTypesModule = module {
single {
AllEditTypes(listOf(
get<QuestTypeRegistry>(),
get<OverlayRegistry>()
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ package de.westnordost.streetcomplete.data.logs

import de.westnordost.streetcomplete.util.ktx.toEpochMilli
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable

@Serializable
data class LogsFilters(
val levels: Set<LogLevel> = LogLevel.entries.toSet(),
val messageContains: String? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.westnordost.streetcomplete.data.osm.edits

import de.westnordost.streetcomplete.data.AllEditTypes
import de.westnordost.streetcomplete.data.CursorPosition
import de.westnordost.streetcomplete.data.Database
import de.westnordost.streetcomplete.data.osm.edits.ElementEditsTable.Columns.ACTION
Expand All @@ -23,9 +24,6 @@ import de.westnordost.streetcomplete.data.osm.edits.move.RevertMoveNodeAction
import de.westnordost.streetcomplete.data.osm.edits.split_way.SplitWayAction
import de.westnordost.streetcomplete.data.osm.edits.update_tags.RevertUpdateElementTagsAction
import de.westnordost.streetcomplete.data.osm.edits.update_tags.UpdateElementTagsAction
import de.westnordost.streetcomplete.data.osm.osmquests.OsmElementQuestType
import de.westnordost.streetcomplete.data.overlays.OverlayRegistry
import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
Expand All @@ -35,8 +33,7 @@ import kotlinx.serialization.modules.subclass

class ElementEditsDao(
private val db: Database,
private val questTypeRegistry: QuestTypeRegistry,
private val overlayRegistry: OverlayRegistry
private val allEditTypes: AllEditTypes,
) {
private val json = Json {
serializersModule = SerializersModule {
Expand Down Expand Up @@ -111,8 +108,7 @@ class ElementEditsDao(

private fun CursorPosition.toElementEdit() = ElementEdit(
getLong(ID),
questTypeRegistry.getByName(getString(QUEST_TYPE)) as? OsmElementQuestType<*>
?: overlayRegistry.getByName(getString(QUEST_TYPE))!!,
allEditTypes.getByName(getString(QUEST_TYPE)) as ElementEditType,
json.decodeFromString(getString(GEOMETRY)),
getString(SOURCE),
getLong(CREATED_TIMESTAMP),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ val elementEditsModule = module {
factory { ChangesetAutoCloser(get()) }
factory { ElementEditUploader(get(), get(), get()) }

factory { ElementEditsDao(get(), get(), get()) }
factory { ElementEditsDao(get(), get()) }
factory { ElementIdProviderDao(get()) }
factory { LastEditTimeStore(get()) }
factory { OpenChangesetsDao(get()) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package de.westnordost.streetcomplete.data.user.achievements

import de.westnordost.streetcomplete.data.AllEditTypes
import de.westnordost.streetcomplete.data.overlays.OverlayRegistry
import de.westnordost.streetcomplete.data.quest.QuestTypeRegistry
import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource
Expand All @@ -11,8 +12,7 @@ class AchievementsController(
private val statisticsSource: StatisticsSource,
private val userAchievementsDao: UserAchievementsDao,
private val userLinksDao: UserLinksDao,
private val questTypeRegistry: QuestTypeRegistry,
private val overlayRegistry: OverlayRegistry,
private val allEditTypes: AllEditTypes,
private val allAchievements: List<Achievement>,
allLinks: List<Link>
) : AchievementsSource {
Expand Down Expand Up @@ -159,12 +159,10 @@ class AchievementsController(
}

private fun isContributingToAchievement(editType: String, achievementId: String): Boolean =
(questTypeRegistry.getByName(editType) ?: overlayRegistry.getByName(editType))
?.achievements?.anyHasId(achievementId) == true
allEditTypes.getByName(editType)?.achievements?.anyHasId(achievementId) == true

private fun getEditTypesContributingToAchievement(achievementId: String): List<String> =
questTypeRegistry.filter { it.achievements.anyHasId(achievementId) }.map { it.name } +
overlayRegistry.filter { it.achievements.anyHasId(achievementId) }.map { it.name }
allEditTypes.filter { it.achievements.anyHasId(achievementId) }.map { it.name }
}

private fun List<EditTypeAchievement>.anyHasId(achievementId: String) = any { it.id == achievementId }
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ val achievementsModule = module {
factory { UserLinksDao(get()) }

single<AchievementsSource> { get<AchievementsController>() }
single { AchievementsController(get(), get(), get(), get(), get(), get(named("Achievements")), get(named("Links"))) }
single { AchievementsController(get(), get(), get(), get(), get(named("Achievements")), get(named("Links"))) }
}

// list of (quest) synonyms (this alternate name is mentioned to aid searching for this code)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.westnordost.streetcomplete.screens.about

import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module

val aboutScreenModule = module {
viewModel<LogsViewModel> { LogsViewModelImpl(get()) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package de.westnordost.streetcomplete.screens.about
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import de.westnordost.streetcomplete.data.logs.LogMessage
import de.westnordost.streetcomplete.databinding.RowLogMessageBinding
Expand All @@ -12,7 +13,8 @@ import kotlinx.datetime.toLocalDateTime

class LogsAdapter : RecyclerView.Adapter<LogsAdapter.ViewHolder>() {

class ViewHolder(private val binding: RowLogMessageBinding) : RecyclerView.ViewHolder(binding.root) {
class ViewHolder(private val binding: RowLogMessageBinding) :
RecyclerView.ViewHolder(binding.root) {
fun onBind(with: LogMessage) {
binding.messageTextView.text = with.toString()

Expand All @@ -29,8 +31,18 @@ class LogsAdapter : RecyclerView.Adapter<LogsAdapter.ViewHolder>() {
var messages: List<LogMessage>
get() = _messages
set(value) {
val result = DiffUtil.calculateDiff(
object : DiffUtil.Callback() {
override fun getOldListSize() = _messages.size
override fun getNewListSize() = value.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
_messages[oldItemPosition].timestamp == value[newItemPosition].timestamp
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
_messages[oldItemPosition] == _messages[newItemPosition]
}
)
_messages = value.toMutableList()
notifyDataSetChanged()
result.dispatchUpdatesTo(this)
}

private var _messages: MutableList<LogMessage> = mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,43 +8,24 @@ import androidx.recyclerview.widget.DividerItemDecoration
import de.westnordost.streetcomplete.BuildConfig
import de.westnordost.streetcomplete.R
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.data.logs.format
import de.westnordost.streetcomplete.databinding.FragmentLogsBinding
import de.westnordost.streetcomplete.screens.TwoPaneDetailFragment
import de.westnordost.streetcomplete.util.ktx.now
import de.westnordost.streetcomplete.util.ktx.systemTimeNow
import de.westnordost.streetcomplete.util.ktx.toEpochMilli
import de.westnordost.streetcomplete.util.ktx.toLocalDate
import de.westnordost.streetcomplete.util.ktx.observe
import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
import de.westnordost.streetcomplete.util.viewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.LocalTime
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel

/** Shows the app logs */
class LogsFragment : TwoPaneDetailFragment(R.layout.fragment_logs) {

private val logsController: LogsController by inject()
private val binding by viewBinding(FragmentLogsBinding::bind)
private val adapter = LogsAdapter()

private var filters: LogsFilters

init {
val startOfToday = LocalDateTime(systemTimeNow().toLocalDate(), LocalTime(0, 0, 0))
filters = LogsFilters(timestampNewerThan = startOfToday)
}
private val viewModel by viewModel<LogsViewModel>()

private val logsControllerListener = object : LogsController.Listener {
override fun onAdded(message: LogMessage) { viewLifecycleScope.launch { onMessageAdded(message) } }
}
private val adapter = LogsAdapter()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -55,27 +36,11 @@ class LogsFragment : TwoPaneDetailFragment(R.layout.fragment_logs) {
binding.logsList.itemAnimator = null // default animations are too slow when logging many messages quickly
binding.logsList.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))

if (savedInstanceState != null) {
onLoadInstanceState(savedInstanceState)
observe(viewModel.logs) { logs ->
adapter.messages = logs
binding.toolbar.root.title = getString(R.string.about_title_logs, logs.size)
binding.logsList.scrollToPosition(logs.lastIndex)
}

showLogs()

logsController.addListener(logsControllerListener)
}

private fun onLoadInstanceState(savedInstanceState: Bundle) {
filters = Json.decodeFromString(savedInstanceState.getString(FILTERS_DATA)!!)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(FILTERS_DATA, Json.encodeToString(filters))
}

override fun onDestroyView() {
super.onDestroyView()
logsController.removeListener(logsControllerListener)
}

private fun createOptionsMenu(toolbar: Toolbar) {
Expand All @@ -97,7 +62,7 @@ class LogsFragment : TwoPaneDetailFragment(R.layout.fragment_logs) {
}

private fun onClickShare() = viewLifecycleScope.launch {
val logText = getLogs().format()
val logText = viewModel.logs.value.format()
val logTimestamp = LocalDateTime.now().toString()
val logTitle = "${BuildConfig.APPLICATION_ID}_${BuildConfig.VERSION_NAME}_$logTimestamp.log"

Expand All @@ -111,46 +76,21 @@ class LogsFragment : TwoPaneDetailFragment(R.layout.fragment_logs) {
}

private fun onClickFilter() {
LogsFiltersDialog(requireContext(), filters) { newFilters ->
LogsFiltersDialog(requireContext(), viewModel.filters.value) { newFilters ->
if (newFilters != null) {
filters = newFilters
showLogs()
viewModel.setFilters(newFilters)
}
}.show()
}

private fun onMessageAdded(message: LogMessage) {
if (filters.matches(message)) {
adapter.add(message)
binding.toolbar.root.title = getString(R.string.about_title_logs, adapter.messages.size)
adapter.add(message)
binding.toolbar.root.title = getString(R.string.about_title_logs, adapter.messages.size)

if (hasScrolledToBottom()) {
binding.logsList.scrollToPosition(adapter.messages.lastIndex)
}
if (binding.logsList.hasScrolledToBottom()) {
binding.logsList.scrollToPosition(adapter.messages.lastIndex)
}
}

private fun showLogs() {
viewLifecycleScope.launch {
val logs = getLogs()
adapter.messages = logs
binding.toolbar.root.title = getString(R.string.about_title_logs, logs.size)
binding.logsList.scrollToPosition(logs.lastIndex)
}
}

private suspend fun getLogs(): List<LogMessage> = withContext(Dispatchers.IO) {
logsController.getLogs(
levels = filters.levels,
messageContains = filters.messageContains,
newerThan = filters.timestampNewerThan?.toEpochMilli(),
olderThan = filters.timestampOlderThan?.toEpochMilli()
)
}

private fun hasScrolledToBottom(): Boolean = !binding.logsList.canScrollVertically(1)

companion object {
private const val FILTERS_DATA = "filters_data"
}
}

private fun View.hasScrolledToBottom(): Boolean = !canScrollVertically(1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.westnordost.streetcomplete.screens.about

import androidx.lifecycle.ViewModel
import de.westnordost.streetcomplete.data.logs.LogMessage
import de.westnordost.streetcomplete.data.logs.LogsFilters
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

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

abstract fun setFilters(filters: LogsFilters)
}
Loading

0 comments on commit ace1fbd

Please sign in to comment.