Home > Mobile >  Can ViewModel observe its MutableLiveData property?
Can ViewModel observe its MutableLiveData property?

Time:10-18

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
    }
}
  • Related