From 351364ec1cd80227d23d412679ad4ac9de97ac08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20Borr=C3=A1s?= Date: Wed, 1 Feb 2023 14:45:42 +0100 Subject: [PATCH 1/4] Use ViewModel on activity --- gradle/libs.versions.toml | 6 ++- taggingviewer/build.gradle.kts | 3 +- .../taggingviewer/DetailTaggingViewModel.kt | 23 ++++++++++ .../taggingviewer/DetailedTaggingActivity.kt | 44 ++++++++++++------- 4 files changed, 58 insertions(+), 18 deletions(-) create mode 100644 taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e908480..a51ab95 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,8 @@ androidx-corektx = "1.9.0" androidx-appcompat = "1.5.0" androidx-recycler = "1.2.1" androidx-livedata = "2.5.1" +androidx-lifecycle = "2.5.1" +androidx-activity = "1.6.1" material = "1.7.0" [libraries] @@ -20,5 +22,7 @@ nexus-staging = { module = "io.codearte.gradle.nexus:gradle-nexus-staging-plugin androidx-coreKtx = { module = "androidx.core:core-ktx", version.ref = "androidx-corektx" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recycler" } -androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "androidx-livedata" } +androidx-activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } +androidx-livedata = { module = "androidx.lifecycle:lifecycle-livedata", version.ref = "androidx-livedata" } +androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } androidx-material = { module = "com.google.android.material:material", version.ref = "material" } \ No newline at end of file diff --git a/taggingviewer/build.gradle.kts b/taggingviewer/build.gradle.kts index 2e252ac..e16296f 100644 --- a/taggingviewer/build.gradle.kts +++ b/taggingviewer/build.gradle.kts @@ -13,7 +13,6 @@ android { compileSdk = 33 defaultConfig { - targetSdk = 33 minSdk = 21 } buildFeatures { @@ -25,6 +24,8 @@ dependencies { implementation(libs.androidx.coreKtx) implementation(libs.androidx.appcompat) implementation(libs.androidx.recyclerView) + implementation(libs.androidx.activity) + implementation(libs.androidx.livedata) implementation(libs.androidx.lifecycle) implementation(libs.androidx.material) } diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt new file mode 100644 index 0000000..3994b9c --- /dev/null +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt @@ -0,0 +1,23 @@ +package com.adevinta.android.taggingviewer + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.adevinta.android.taggingviewer.internal.TagEntry +import com.adevinta.android.taggingviewer.internal.TrackingDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +class DetailTaggingViewModel : ViewModel() { + + private val _flow: MutableStateFlow> = MutableStateFlow(emptyList()) + val flow: StateFlow> = _flow.asStateFlow() + + fun onInit(lifecycleOwner: LifecycleOwner) = viewModelScope.launch { + TrackingDispatcher.entriesData().observe(lifecycleOwner) { entries -> + _flow.value = entries + } + } +} diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt index 38b0660..a040b71 100644 --- a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt @@ -9,22 +9,31 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuHost import androidx.core.view.MenuProvider +import androidx.lifecycle.Lifecycle import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.adevinta.android.taggingviewer.databinding.TaggingActivityDetailedBinding import com.adevinta.android.taggingviewer.filter.TaggingViewerFilterListBottomSheet import com.adevinta.android.taggingviewer.internal.TagEntry import com.adevinta.android.taggingviewer.internal.TrackingDispatcher +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Date import java.util.Locale class DetailedTaggingActivity : AppCompatActivity() { + private val viewModel: DetailTaggingViewModel by viewModels() + private val adapter: DetailTaggingAdapter = DetailTaggingAdapter() private var itemTypes: MutableMap = mutableMapOf() @@ -38,26 +47,26 @@ class DetailedTaggingActivity : AppCompatActivity() { binding.detailedTaggingListView.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) binding.detailedTaggingListView.adapter = adapter - TrackingDispatcher.entriesData().observe( - this, - Observer { entries -> - val filteredEntries = entries - .sortedByDescending { it.timestamp } - .filterNot { it is TagEntry.SeparatorEntry } - .filter { itemTypes[it.name] ?: true } + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + viewModel.flow.collect { entries -> + val filteredEntries = entries + .sortedByDescending { it.timestamp } + .filterNot { it is TagEntry.SeparatorEntry } + .filter { itemTypes[it.name] ?: true } - adapter.entries = filteredEntries + adapter.entries = filteredEntries - itemTypes = filteredEntries - .map { it.name } - .distinct() - .associateWith { true } - .toMutableMap() + itemTypes = filteredEntries + .map { it.name } + .distinct() + .associateWith { true } + .toMutableMap() - (this as MenuHost).invalidateMenu() + (this@DetailedTaggingActivity as MenuHost).invalidateMenu() + } } - ) - + } addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { @@ -85,6 +94,9 @@ class DetailedTaggingActivity : AppCompatActivity() { } } }, this) + + + viewModel.onInit(this) } private fun showFilterList() { From b4216b6fbb29098cd7867ab6eab7fd6c5a6275f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20Borr=C3=A1s?= Date: Wed, 1 Feb 2023 14:51:45 +0100 Subject: [PATCH 2/4] Move delete --- .../adevinta/android/taggingviewer/DetailTaggingViewModel.kt | 4 ++++ .../adevinta/android/taggingviewer/DetailedTaggingActivity.kt | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt index 3994b9c..e043248 100644 --- a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt @@ -20,4 +20,8 @@ class DetailTaggingViewModel : ViewModel() { _flow.value = entries } } + + fun clearAll() = viewModelScope.launch { + TaggingViewer.clearAll() + } } diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt index a040b71..d1cb8ef 100644 --- a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt @@ -82,7 +82,7 @@ class DetailedTaggingActivity : AppCompatActivity() { override fun onMenuItemSelected(menuItem: MenuItem): Boolean { return when (menuItem.itemId) { R.id.menu_remove -> { - TaggingViewer.clearAll() + viewModel.clearAll() adapter.entries = listOf() true } From 439ba7669bc9213fffe809686d0730b0b658eb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20Borr=C3=A1s?= Date: Wed, 1 Feb 2023 15:07:53 +0100 Subject: [PATCH 3/4] Use VM to filter --- .../taggingviewer/DetailTaggingViewModel.kt | 40 +++++++++++++- .../taggingviewer/DetailedTaggingActivity.kt | 53 +++++++------------ 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt index e043248..45a9b5d 100644 --- a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingViewModel.kt @@ -12,16 +12,52 @@ import kotlinx.coroutines.launch class DetailTaggingViewModel : ViewModel() { - private val _flow: MutableStateFlow> = MutableStateFlow(emptyList()) + private var currentEntries: List = emptyList() + + private var itemTypes: MutableMap = mutableMapOf() + + private val _flow: MutableStateFlow> = MutableStateFlow(currentEntries) val flow: StateFlow> = _flow.asStateFlow() + private val _filtersShown: MutableStateFlow?> = MutableStateFlow(null) + val filterShown: StateFlow?> = _filtersShown.asStateFlow() + fun onInit(lifecycleOwner: LifecycleOwner) = viewModelScope.launch { TrackingDispatcher.entriesData().observe(lifecycleOwner) { entries -> - _flow.value = entries + currentEntries = entries + + itemTypes = currentEntries + .map { it.name } + .filterNot { it.isEmpty() } + .distinct() + .associateWith { key -> itemTypes[key] ?: true } + .toMutableMap() + + sendEntries() } } + private fun sendEntries() { + _flow.value = currentEntries + .sortedByDescending { it.timestamp } + .filterNot { it is TagEntry.SeparatorEntry } + .filter { itemTypes[it.name] ?: true } + } + fun clearAll() = viewModelScope.launch { TaggingViewer.clearAll() } + + fun onFilterUpdated(type: String, visible: Boolean) { + itemTypes[type] = visible + sendEntries() + } + + fun onShowFilters() = viewModelScope.launch { + _filtersShown.value = itemTypes + } + + fun onFiltersShown() = viewModelScope.launch { + _filtersShown.value = null + } } diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt index d1cb8ef..7ee8bfc 100644 --- a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt @@ -14,8 +14,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.MenuHost import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager @@ -23,8 +21,6 @@ import androidx.recyclerview.widget.RecyclerView import com.adevinta.android.taggingviewer.databinding.TaggingActivityDetailedBinding import com.adevinta.android.taggingviewer.filter.TaggingViewerFilterListBottomSheet import com.adevinta.android.taggingviewer.internal.TagEntry -import com.adevinta.android.taggingviewer.internal.TrackingDispatcher -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Date @@ -36,8 +32,6 @@ class DetailedTaggingActivity : AppCompatActivity() { private val adapter: DetailTaggingAdapter = DetailTaggingAdapter() - private var itemTypes: MutableMap = mutableMapOf() - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = TaggingActivityDetailedBinding.inflate(layoutInflater) @@ -49,22 +43,18 @@ class DetailedTaggingActivity : AppCompatActivity() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.flow.collect { entries -> - val filteredEntries = entries - .sortedByDescending { it.timestamp } - .filterNot { it is TagEntry.SeparatorEntry } - .filter { itemTypes[it.name] ?: true } - - adapter.entries = filteredEntries - - itemTypes = filteredEntries - .map { it.name } - .distinct() - .associateWith { true } - .toMutableMap() - - (this@DetailedTaggingActivity as MenuHost).invalidateMenu() - } + launch { + viewModel.flow.collect { entries -> + adapter.entries = entries + (this@DetailedTaggingActivity as MenuHost).invalidateMenu() + } + } + + launch { + viewModel.filterShown.collect { filters -> + filters?.let { showFilterList(it) } + } + } } } @@ -76,7 +66,7 @@ class DetailedTaggingActivity : AppCompatActivity() { override fun onPrepareMenu(menu: Menu) { super.onPrepareMenu(menu) - menu.findItem(R.id.menu_filter).isVisible = itemTypes.isNotEmpty() + menu.findItem(R.id.menu_filter).isEnabled = adapter.entries.isNotEmpty() } override fun onMenuItemSelected(menuItem: MenuItem): Boolean { @@ -87,7 +77,7 @@ class DetailedTaggingActivity : AppCompatActivity() { true } R.id.menu_filter -> { - showFilterList() + viewModel.onShowFilters() true } else -> false @@ -99,27 +89,20 @@ class DetailedTaggingActivity : AppCompatActivity() { viewModel.onInit(this) } - private fun showFilterList() { + private fun showFilterList(filters: Map) { + viewModel.onFiltersShown() TaggingViewerFilterListBottomSheet.show( fm = supportFragmentManager, - itemTypes = itemTypes, + itemTypes = filters, onTypeVisibilityChanged = { type, visible -> - itemTypes[type] = visible - adapter.filter = itemTypes + viewModel.onFilterUpdated(type, visible) }, ) } } internal class DetailTaggingAdapter : RecyclerView.Adapter() { - var filter: MutableMap = mutableMapOf() - set(value) { - field = value - notifyDataSetChanged() - } - var entries: List = mutableListOf() - get() = field.filter { filter[it.name] ?: true } set(value) { field = value notifyDataSetChanged() From c98e82a0c7981bc95f46b7018189ee7bf962c9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernat=20Borr=C3=A1s?= Date: Wed, 1 Feb 2023 15:11:14 +0100 Subject: [PATCH 4/4] Move adapter out --- .../taggingviewer/DetailTaggingAdapter.kt | 77 +++++++++++++++++++ .../taggingviewer/DetailedTaggingActivity.kt | 68 ---------------- .../android/taggingviewer/ListPosition.kt | 5 ++ 3 files changed, 82 insertions(+), 68 deletions(-) create mode 100644 taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingAdapter.kt create mode 100644 taggingviewer/src/main/java/com/adevinta/android/taggingviewer/ListPosition.kt diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingAdapter.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingAdapter.kt new file mode 100644 index 0000000..f63bd12 --- /dev/null +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailTaggingAdapter.kt @@ -0,0 +1,77 @@ +package com.adevinta.android.taggingviewer + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.adevinta.android.taggingviewer.databinding.TaggingDetailedItemBinding +import com.adevinta.android.taggingviewer.internal.TagEntry +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +internal class DetailTaggingAdapter : RecyclerView.Adapter() { + var entries: List = mutableListOf() + set(value) { + field = value + notifyDataSetChanged() + } + + override fun getItemCount() = entries.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailTaggingViewHolder { + val binding = TaggingDetailedItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return DetailTaggingViewHolder(binding) + } + + override fun onBindViewHolder(holder: DetailTaggingViewHolder, position: Int) { + val listPosition = when (position) { + 0 -> ListPosition.FIRST + itemCount - 1 -> ListPosition.LAST + else -> ListPosition.MIDDLE + } + holder.bind(entries[position], listPosition) + } +} + +internal class DetailTaggingViewHolder(binding: TaggingDetailedItemBinding) : RecyclerView.ViewHolder(binding.root) { + + private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) + + fun bind(tagEntry: TagEntry, listPosition: ListPosition) { + val binding = TaggingDetailedItemBinding.bind(itemView) + + binding.detailItemTitle.text = tagEntry.name + + with(binding.detailItemExtra) { + if (tagEntry is TagEntry.ItemEntry) { + if (tagEntry.details.isEmpty()) { + visibility = View.GONE + } else { + visibility = View.VISIBLE + text = tagEntry.details + .map { (key, value) -> "$key: $value" } + .joinToString("\n") + } + } + } + + binding.detailItemTime.text = timeFormat.format(Date(tagEntry.timestamp)) + + binding.detailItemIcon.setImageResource( + when (tagEntry) { + is TagEntry.ItemEntry.Click -> R.drawable.tgv_marker_click + is TagEntry.ItemEntry.Event -> R.drawable.tgv_marker_event + is TagEntry.ItemEntry.Screen -> R.drawable.tgv_marker_screen + is TagEntry.ItemEntry.UserAttribute -> R.drawable.tgv_marker_user_attribute + else -> error("No icon assigned to $tagEntry") + } + ) + + binding.detailItemLineBottom.visibility = if (listPosition == ListPosition.LAST) { + View.INVISIBLE + } else { + View.VISIBLE + } + } +} diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt index 7ee8bfc..a45fc20 100644 --- a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/DetailedTaggingActivity.kt @@ -100,71 +100,3 @@ class DetailedTaggingActivity : AppCompatActivity() { ) } } - -internal class DetailTaggingAdapter : RecyclerView.Adapter() { - var entries: List = mutableListOf() - set(value) { - field = value - notifyDataSetChanged() - } - - override fun getItemCount() = entries.size - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DetailTaggingViewHolder { - return DetailTaggingViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.tagging_detailed_item, parent, false)) - } - - override fun onBindViewHolder(holder: DetailTaggingViewHolder, position: Int) { - val listPosition = when (position) { - 0 -> ListPosition.FIRST - itemCount - 1 -> ListPosition.LAST - else -> ListPosition.MIDDLE - } - holder.bind(entries[position], listPosition) - } -} - -internal class DetailTaggingViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - private val timeFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - - fun bind(tagEntry: TagEntry, listPosition: ListPosition) { - itemView.findViewById(R.id.detailItemTitle).text = tagEntry.name - - itemView.findViewById(R.id.detailItemExtra).apply { - if (tagEntry is TagEntry.ItemEntry) { - if (tagEntry.details.isEmpty()) { - visibility = View.GONE - } else { - visibility = View.VISIBLE - text = tagEntry.details - .map { (key, value) -> "$key: $value" } - .joinToString("\n") - } - } - } - - itemView.findViewById(R.id.detailItemTime).text = timeFormat.format(Date(tagEntry.timestamp)) - - itemView.findViewById(R.id.detailItemIcon).setImageResource( - when (tagEntry) { - is TagEntry.ItemEntry.Click -> R.drawable.tgv_marker_click - is TagEntry.ItemEntry.Event -> R.drawable.tgv_marker_event - is TagEntry.ItemEntry.Screen -> R.drawable.tgv_marker_screen - is TagEntry.ItemEntry.UserAttribute -> R.drawable.tgv_marker_user_attribute - else -> error("No icon assigned to $tagEntry") - } - ) - - itemView.findViewById(R.id.detailItemLineBottom).visibility = - if (listPosition == ListPosition.LAST) { - View.INVISIBLE - } else { - View.VISIBLE - } - } -} - -internal enum class ListPosition { - FIRST, LAST, MIDDLE -} diff --git a/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/ListPosition.kt b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/ListPosition.kt new file mode 100644 index 0000000..480675b --- /dev/null +++ b/taggingviewer/src/main/java/com/adevinta/android/taggingviewer/ListPosition.kt @@ -0,0 +1,5 @@ +package com.adevinta.android.taggingviewer + +internal enum class ListPosition { + FIRST, LAST, MIDDLE +}