Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TECH: Use view model to load data #15

Merged
merged 4 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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" }
3 changes: 2 additions & 1 deletion taggingviewer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ android {
compileSdk = 33

defaultConfig {
targetSdk = 33
minSdk = 21
}
buildFeatures {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DetailTaggingViewHolder>() {
var entries: List<TagEntry> = 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
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
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 var currentEntries: List<TagEntry> = emptyList()

private var itemTypes: MutableMap<String, Boolean> = mutableMapOf()

private val _flow: MutableStateFlow<List<TagEntry>> = MutableStateFlow(currentEntries)
val flow: StateFlow<List<TagEntry>> = _flow.asStateFlow()

private val _filtersShown: MutableStateFlow<Map<String, Boolean>?> = MutableStateFlow(null)
val filterShown: StateFlow<Map<String, Boolean>?> = _filtersShown.asStateFlow()

fun onInit(lifecycleOwner: LifecycleOwner) = viewModelScope.launch {
TrackingDispatcher.entriesData().observe(lifecycleOwner) { 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,28 @@ 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.Observer
import androidx.lifecycle.Lifecycle
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.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class DetailedTaggingActivity : AppCompatActivity() {

private val adapter: DetailTaggingAdapter = DetailTaggingAdapter()
private val viewModel: DetailTaggingViewModel by viewModels()

private var itemTypes: MutableMap<String, Boolean> = mutableMapOf()
private val adapter: DetailTaggingAdapter = DetailTaggingAdapter()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -38,26 +41,22 @@ 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 }

adapter.entries = filteredEntries

itemTypes = filteredEntries
.map { it.name }
.distinct()
.associateWith { true }
.toMutableMap()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch {
viewModel.flow.collect { entries ->
adapter.entries = entries
(this@DetailedTaggingActivity as MenuHost).invalidateMenu()
}
}

(this as MenuHost).invalidateMenu()
launch {
viewModel.filterShown.collect { filters ->
filters?.let { showFilterList(it) }
}
}
}
)

}

addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
Expand All @@ -67,109 +66,37 @@ 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 {
return when (menuItem.itemId) {
R.id.menu_remove -> {
TaggingViewer.clearAll()
viewModel.clearAll()
adapter.entries = listOf()
true
}
R.id.menu_filter -> {
showFilterList()
viewModel.onShowFilters()
true
}
else -> false
}
}
}, this)


viewModel.onInit(this)
}

private fun showFilterList() {
private fun showFilterList(filters: Map<String, Boolean>) {
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<DetailTaggingViewHolder>() {
var filter: MutableMap<String, Boolean> = mutableMapOf()
set(value) {
field = value
notifyDataSetChanged()
}

var entries: List<TagEntry> = mutableListOf()
get() = field.filter { filter[it.name] ?: true }
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<TextView>(R.id.detailItemTitle).text = tagEntry.name

itemView.findViewById<TextView>(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<TextView>(R.id.detailItemTime).text = timeFormat.format(Date(tagEntry.timestamp))

itemView.findViewById<ImageView>(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<View>(R.id.detailItemLineBottom).visibility =
if (listPosition == ListPosition.LAST) {
View.INVISIBLE
} else {
View.VISIBLE
}
}
}

internal enum class ListPosition {
FIRST, LAST, MIDDLE
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.adevinta.android.taggingviewer

internal enum class ListPosition {
FIRST, LAST, MIDDLE
}