android / architecture-components-samples

Samples for Android Architecture Components.
https://d.android.com/arch
Apache License 2.0
23.42k stars 8.29k forks source link

LiveData and Adapters #135

Closed matejdro closed 7 years ago

matejdro commented 7 years ago

What is the best way to handle adapter updates with data arriving only through LiveData?

With everything in Activity, you could call all methods, such as adapter.notifyItemAdded(), but with LiveData you can only submit one type of item, generally whole list to display. That way you can only call adapter.notifyDataSetChanged() which documentation states it should be used as last resort.

So is there any better way to do it, or do we just need to call adapter.notifyDataSetChanged() from now on?

andhie commented 7 years ago

You can use v7 DiffUtil https://developer.android.com/reference/android/support/v7/util/DiffUtil.html

matejdro commented 7 years ago

Right, but this still throws away the information I have. For example If I add single item to a list in a ViewModel, there is no good way to tell the activity that. I still have to throw away that information, post entire new list and check what happened with DiffUtil.

justjanne commented 7 years ago

I’d like to know this as well – I’m working with large (tenthousands of items) datasets, and need to handle their LiveData<List<...>> changing frequently. Just diffing them is a major waste of time. A LiveDataCollection type that can handle collections, and changes within them, would be a major improvement.

ChrisCraik commented 7 years ago

You can record the operations on the provider side, and dispatch the notifyItem... messages yourself, alongside the new list, something like: LiveData<Pair<List<>, MyListUpdate>>. You might find something useful in SortedList as well.

Alternately, if you're OK with diffing and just want it to be easier - ListAdapter is part of the new Paging library released in Architecture Components last week. It can manage diffing lists for you on a background thread, and can be easily bound to a LiveData<List<...>>

LiveData<List<Item>> myLiveDataList = ...;

myLiveDataList.observe(this, list -> listAdapter.setList(list));

There's also a PagedListAdapter, which does the same thing, but also handles updates within a arbitrarily large, lazy-loaded list. For PagedListAdapter specifically, it only diffs the items loaded in memory, and you can just simply call a getter for the item. All of the notifyItem... calls are handled for you.

Both of the adapters use DiffUtil to do the diffing work for you, but they can't be told when there's a specific known update - they'll always do a diff on all the loaded data. Doing that work on a background thread helps though, as does paging - if you have less of each dataset loaded when you're sending a new list, less diffing computation is needed.

makk909 commented 6 years ago

I don't think any of the solutions mentioned above has related to the Original Question, I want this issue to be reopened as currently I find no way to update a single item in RecyclerView Adapter using LiveData inside ViewModel, we have to set entire list everytime which I don't think a better solution or technique.

jkwiecien commented 6 years ago

+1 to reopening, I don't see a way to add, update or remove single item with a combination of PagedList, PagedListAdapter and PageKeyedDataSource (for instance)

Simple case: Screen contains list of network fetched items. Element can be swipe-deleted. There is no way of telling any of the mentioned classes that it should just remove a single item from the list.

Same goes for adding or updating.

florina-muntenescu commented 6 years ago

Since this is not an issue specifically related to any of the samples, please create an issue on the official issue tracker for Architecture Components.

yccheok commented 6 years ago

Hey all, I had made an official feature request on this common operation - https://issuetracker.google.com/issues/77363656 :) If you like the feature request, please kindly stared it :)

vishnuharidas commented 5 years ago

If you are in control of adding each item to the list, then here is a simple solution to use an accompanying flag to notify the Activity. For example,

// inside ViewModel
val animalsList = ArrayList<Animal>()
val animalsListUpdated: MutableLiveData<Long> = MutableLiveData() // flag

// adding items to the list
fun updateList(item: Animal){
    animalList.add(item)
    animalListUpdated.value = System.currentTimeMillis() // updating the flag
}

// inside Activity
val adapter = Adapter(viewModel.animalsList)
// observing changes
viewModel.animalsListUpdated.observe(this, Observer { value -> 
    adapter.notifyDatasetChanged()
} 
matejdro commented 5 years ago

I feel like this is the worst solution. You should pass immutable lists through LiveData.

vishnuharidas commented 5 years ago

I feel like this is the worst solution. You should pass immutable lists through LiveData.

Don't worry, a small change will make the LiveData immutable in the above example:

private val _animalsListUpdated: MutableLiveData<Long> = MutableLiveData()
val animalsListUpdated: LiveData<Long> get() = _animalsListUpdated
matejdro commented 5 years ago

Yes, but list itself is not immutable. You cannot for example update that list freely on the background thread without worying that Android might just be in the middle of the layout phase and drawing that list, causing concurrent modification exceptions.

vishnuharidas commented 5 years ago

Yes, but list itself is not immutable. You cannot for example update that list freely on the background thread without worying that Android might just be in the middle of the layout phase and drawing that list, causing concurrent modification exceptions.

If you are talking about someone outside of the ViewModel updating the list, we can provide an immutable list always. Applying the same logic as above,

private val _items: MutableList<Animal> = ArrayList()
val items: List<Animal> get() = _items

Is that what you meant?

matejdro commented 5 years ago

No, I'm talking about updating inside viewmodel. This is the better way to do it if you really want to avoid LiveData:

var items: List<Animal> = emptyList()
private set

Note that list is not mutable. You can only swap out whole list, but not modify existing one. Like this:

animalList = animalList.toMutableList().apply { add(item) }
vishnuharidas commented 5 years ago

So is there any solution for this yet?

matejdro commented 5 years ago

It does not look like it. I've just given up and switched to DiffUtils now. Yes, it is a bit less efficient, but it is much more convenient.

ivanempire commented 5 years ago

@matejdro Does your project have Rx in it? That would kind of solve everything because they have RxAdapter for exactly this kind of stuff. It's a bummer this isn't implemented. :(

matejdro commented 5 years ago

Yes, but then I loose all benefits of LiveData (Lifecycle awareness).

ivanempire commented 5 years ago

@matejdro Yeah that's fair, although we do handle that manually by using Trello-lifecycle. 😄 Something I did the other day for this, which seemed to work is the following. I'm scanning for BLE devices and displaying them in a list with a custom adapter, so, whenever a new item is discovered, I have:

val newlyDiscoveredDevice = MutableLiveData<RxBleDevice>()

and I have the observer updating the data:

myClass.newlyDiscoveredDevice.observe(this, Observer {
    deviceAdapter.add(it)
})

I don't know it something will leak, or if this is a really bad idea but..seems to be OK?

matejdro commented 5 years ago

This is really offtopic, but I think it would be easier for you if you would collect devices in the data layer and push List<RxBleDevice> through LiveData or Rx or whatever you use.

ivanempire commented 5 years ago

Yeah my bad, I didn't mean to throw the thread off-topic, just giving my two cents. I'll check that out! :D

abhriyaroy commented 4 years ago

Hey so maybe I am very late, but the best way to implement this seems to be using DiffUtil for a recyclerview.

alexgomes09 commented 1 year ago

2023 yet there's really no way other than using DiffUtils. :(

ivanempire commented 1 year ago

I think at this point you're better off using Kotlin Flows and Flow operators + lazy layouts in Jetpack Compose.