Skip to content

Commit

Permalink
Merge pull request #15 from AdevintaSpain/use_vioew_model
Browse files Browse the repository at this point in the history
TECH: Use view model to load data
  • Loading branch information
alorma authored Feb 1, 2023
2 parents e5b8f2c + c98e82a commit ee92555
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 106 deletions.
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
}

0 comments on commit ee92555

Please sign in to comment.