I'm trying to use ViewModels with LiveData
and MutableLiveData
in Android for the first time. I have not read all documentation yet.
My question:
How can I call fun applyFilters()
when val filterByName = MutableLiveData<String>()
was changed from outside (from a Fragment
).
In .NET MVVM I would just add that call in the setter for filterByName
property. But how to I do it in with MutableLiveData
?
I think I should use MediatorLiveData
, but this documentation does not explain much.
I was thinking about using the observer
inside a viewmodel (so it could observe itself), but then I would have to use AndroidViewModel
instead of ViewModel
, to get lifecycleOwner
which is required to observe MutableLiveData...
My code:
In my AssignUhfTagToInventoryItemViewModel() : ViewModel()
(which is shared between few fragments) I have properties like this:
/* This is filled only once in SomeViewModel.init() function, always contains all data */
var _items = ArrayList<InventoryItemDto?>()
/* This is filled from _items, contains filtered data */
val items = MutableLiveData<ArrayList<InventoryItemDto?>>().apply { value = ArrayList() }
val filterByName = MutableLiveData<String>().apply { value = "" }
And a function like this:
fun applyFilters(){
Log.d(TAG, "ViewModel apply filters called. Current name filter: ${filterByName.value}")
items.value?.clear()
val nameFilterLowercase = filterByName.value.toString().lowercase()
if (!filterByName.value.isNullOrEmpty()) {
for (item in _items) {
val itemNameLowercase = item?.name?.lowercase()
if (itemNameLowercase?.contains(nameFilterLowercase) == true)
items.value?.add(item)
}
Log.d(TAG, "Filter is on, data cached : ${_items.count()} rows")
Log.d(TAG, " data displayed: ${items.value?.count()} rows")
}
else {
items.value?.addAll(_items)
Log.d(TAG, "Filter is off, data cached : ${_items.count()} rows")
Log.d(TAG, " data displayed: ${items.value?.count()} rows")
}
}
And this is part of my Fragment
where I use that viewmodel:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentInventoryItemSelectBinding.bind(view)
val vm = ViewModelProvider(requireActivity())[AssignUhfTagToInventoryItemViewModel::class.java]
// old school ArrayAdapter
val listAdapter = InventoryItemViewAdapter(this.context, vm.items.value)
binding.lvItems.adapter = listAdapter // old school ListView
binding.tvFilterByName.addTextChangedListener(AfterTextChangedWatcher {
vm.filterByName.postValue(it)
/*
IN MY FRAGMENT I WANT TO AVOID FUNCTION CALL BELOW
*/
vm.applyFilters()
listAdapter.notifyDataSetInvalidated()
Log.d(TAG, "afterTextChanged: $it")
})
vm.items.observe(requireActivity()){
listAdapter.notifyDataSetInvalidated()
}
}
CodePudding user response:
Technically? I guess you can but you can use Transformations.switchMap
to take input from one LiveData and apply them to the source from another.
var _items = MutableLiveData(ArrayList<InventoryItemDto?>())
val filterByName = MutableLiveData<String>().apply { value = "" }
val items = Transformations.switchMap(filterByName) { filter ->
val nameFilterLowercase = filter.lowercase()
_items.map { mappedItems ->
mappedItems.filter { listItem ->
listItem.contains(nameFilterLowercase)
}
}
}
vm.items.observe(viewLifecycleOwner) { items ->
// old school ArrayAdapter
val listAdapter = InventoryItemViewAdapter(this.context, items)
binding.lvItems.adapter = listAdapter // old school ListView
}
CodePudding user response:
The view models have a last call method where you could clear the observers: onCleared.
In your case, it seems that the filters depend on the items; in that case, is better to use Transformations.map
So it would be something like this:
val filterByName = items.map {
//do your thing here
}
If you need any other value or skip emissions you can take a look at Mediator
and use it something like this.
val filterByName = MediatorLiveData<String>.apply {
addSource(_items) {
//you still need to map your items to a filter but you have to update manually
value = //whatever your calculation
}
}