notifyItemInserted renders the entire list again - android

I have the following method -
private fun initRoomObserving() {
dashboardViewModel = ViewModelProvider(this).get(DashboardViewModel::class.java)
dashboardViewModel.getAllMessagesEntities().observe(this, Observer { receivedMessageList ->
receivedMessageList.forEach {
if (!userPhoneNumber.equals(it.senderUsername)) {
it.isReceiver = true
}
if (!messagesList.contains(it)) {
messagesList.add(it)
}
}
conversationAdapter.notifyItemInserted(messagesList.size)
conversationAdapter.notifyItemRangeChanged(messagesList.size - 1,messagesList.size)
})
}
For some reason the entire list is being rendered again for each time a new entity is beining added, even though I am explicitly notifyItemInserted and not notifyDataSetChanged
Why is this happening and what am I missing?

The seconds parameter for notifyItemRangeChanged takes the count as value so since the value of changes item is always one so pass 1 instead of messagesList.size as:
conversationAdapter.notifyItemRangeChanged(messagesList.size - 1, 1)
Additionally, someCount can be variable which can track the number of changed items which can be used for notifyItemRangeChanged when you will have more then one item for updates.

Related

Removing elements from a list that is used by liveData does not remove any

Android 4.1.2
Kotlin 1.4.21
I have the following live data that I add to, but when it comes to removing it doesn't remove any elements.
val selectedLiveData by lazy { MutableLiveData<List<Core>>() }
I don't want to trigger the observers so I am not assigning the value as I just want to remove a single element from the liveData list and only trigger when adding.
None of the following work
selectedLiveData.value?.toMutableList()?.apply {
removeAt(0)
}
selectedLiveData.value?.toMutableList()?.apply {
removeFirst()
}
selectedLiveData.value?.toMutableList()?.apply {
remove(Core)
}
I am adding my elements like this and then assigning the value so the observers to this live data get updated:
selectedLiveData.value = selectedLiveData.value?.toMutableList()?.apply {
add(core)
}
What you wanted is
val selectedLiveData = MutableLiveData<List<Core>>(emptyList())
Then
selectedLiveData.value = selectedLiveData.value.toMutableList().apply {
removeAt(0)
}.toList()
So what are you doing exactly:
You create a MutableLiveData with a List of objects. As we know in Kotlin List is immutable, so it's readonly.
If you want to add / remove items from a list, you should use MutableList.
If we look the documentation of toMutableList which you are using:
/**
* Returns a new [MutableList] filled with all elements of this collection.
*/
public fun <T> Collection<T>.toMutableList(): MutableList<T> {
return ArrayList(this)
}
So every time you try to remove an item via:
selectedLiveData.value?.toMutableList()
you actually perform that operation on a new MutableList not the original one.
If you want to add / remove I suggest you to use MutableList in your MutableLiveData so you can create something similar to this:
private val selectedLiveData = MutableLiveData<MutableList<Int>>()
// Init
selectedLiveData.value = mutableListOf(100, 200)
// Add items
selectedLiveData.value?.add(2)
selectedLiveData.value?.add(10)
selectedLiveData.value?.add(50)
// Remove item
selectedLiveData.value?.remove(2)
selectedLiveData.postValue(selectedLiveData.value.toMutableList().apply {
removeAt(0)
}.toList())

Android - Add new Items to MutableLiveData and observe

How can I add new Items to a MutableLiveData List? I want to build an infinite scrolling Recyclerview. So I have a List with 10 items and after clicking a "load more button" I want to add ten new Items.
This are my lists of products:
private companion object {
private var _products = MutableLiveData<MutableList<Product>>()
}
val products: LiveData<MutableList<Product>> = _products
Ant this is my loading function:
fun loadProducts(category: Category) { // category: String
_isViewLoading.postValue(true)
AppService.fetchProducts(category, neuheitenCounter) { success, response ->
_isViewLoading.postValue(false)
if(success) {
when(category) {
Category.NEWS -> {
if(_products.value == null) {
_products.value = response?.products //as List<Product>
} else {
response?.let {
_products.value?.addAll(response?.products)
}
}
neuheitenCounter++
}
}
}
}
}
If I call _products.value = response?.products it fires the observe method in my Activity. But if I call addAll or add, the observer method is not called.
Actually, it's how you should update the adapter
Add new items to the existing list
Notify LiveData by assigning the same list to its value. Here is the same question with the solution.
When your Activity is notified that the list is changed, it should notify the adapter about the changes by calling its notifyDataSetChanged() method.
When you call notifyDataSetChanged() method of the adapter, it compares previous items with the new ones, figures out which of them are changed and updates the RecyclerView. You can help the adapter by calling notifyItemRangeInserted() instead of notifyDataSetChanged(). notifyItemRangeInserted() accepts a range on inserted items, in your case you can calculate it but taking the difference between adapter.itemCount and the productList.size.
MutableLiveData.postValue(T value)
I had that same issue .I copied data of LiveData in new List then i added new data to new List.And assigned new List to LiveData.I know this not exact SOLUTION but this solved problem.
Try this.
var tempList= _products.value
tempList.addAll(response?.products)
_products.value = tempList
I hope this will help you.

How to wait for ListAdapter.submitList() to execute and call RecyclerView.smoothScrollToPosition(0);

I have a RecyclerView recyclerView which is linked to a ListAdapter adapter. The 'list' gets new data from Firebase database and the 'list' gets sorted based on certain object properties. I then call adapter.submitList(list) to update the recyclerView - this works perfectly!
But, I want the recyclerView to scroll to the top of the list after the new data is added. But I see that adapter.submitList(list) runs on a separate thread. So how would I go about knowing when it is done submitting and call recyclerView.smoothScrollToPosition(0);
Let me know if I should provide more details.
ListAdapter has got another overloaded version of submitList() ListAdapter#submitList(#Nullable List<T> list, #Nullable final Runnable commitCallback) that takes second argument as Runnable.
As per the documentation:
The commit callback can be used to know when the List is committed,
but note that it
* may not be executed. If List B is submitted immediately after List A, and is
* committed directly, the callback associated with List A will not be run.
Usage:
listAdapter.submitList(*your_new_list*, new Runnable() {
#Override
public void run() {
// do your stuff
}
});
You can use an AdapterDataObserver on your recyclerview adapter, that will happen after submit finish :
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
when {
positionStart == 0 && itemCount != 0 -> {
mRecyclerView.smoothScrollToPosition(itemCount)
}
positionStart > 0 -> {
mRecyclerView.smoothScrollToPosition(adapter.itemCount)
}
}
}
})
Be careful with positionStart, item count value. There is also other callbacks.
As #Chintan Soni said, you can make use of the submitList commit callback. Otherwise, and in case you're using kotlin coroutines and want to wait for animations to end, I would suggest getting a look at the awaitSomething() pattern.
recyclerView.awaitScrollEnd() // custom suspend extension function
more by Chris Banes on : https://medium.com/androiddevelopers/suspending-over-views-19de9ebd7020

Comparing and replacing items in two lists with different sizes in Kotlin?

I have the following function:
override fun insertUpdatedItems(items: List<AutomobileEntity>) {
if (!items.isEmpty()) {
items.forEachIndexed { index, automobileEntity ->
if (automobileEntity.id == items[index].id) {
automobileCollection[index] = items[index]
notifyItemInserted(index)
}
}
}
}
I'm using to provide data for a recyclerview, I'm trying to insert updated/edited items that are already in automobileCollection which size always returns 10 items but the items list might differ it can be 1 to 10.
It's supposed to compare items by id but what I'm getting currently with this function is the edited items are just inserted to recyclerview's adapter and not treated as an already existing item.
On the contrary, if I iterate using automobileCollection I get IndexOutOfBoundsException since most of the time the items list is smaller than automobileCollection.
To update a list with items from another one, you can use several ways.
First starting with a direct replacement (which preserves the order, but that's just a detail):
val sourceList = TODO()
val targetList = TODO()
targetList.replaceAll { targetItem ->
sourceList.firstOrNull { targetItem.id == it.id }
?: targetItem
}
Alternatively removing all the items and adding them again:
targetList.removeIf { targetItem ->
sourceList.any { it.id == targetItem.id }
}
targetList.addAll(sourceList)
Using listIterator (note! that's actually also happening under the hood when you call replaceAll... not in the same way, but similar ;-)):
val iterator = targetList.listIterator()
while (iterator.hasNext()) {
iterator.next().apply {
sourceList.firstOrNull { id == it.id }?.also(iterator::set)
}
}
Probably not so readable... For your forEachIndexed I do not really see any use-case. For other problems there definitely are, but I would suggest you try to omit indices (and also forEach) as often as you can. If nothing better comes to your mind, then forEach is also ok, but many times, forEach (and even more so forEachIndexed) isn't the best approach to solve an issue.

How to properly update Android's RecyclerView using LiveData?

Bottom Line Question
If I'm using MutableLiveData<List<Article>>, is there a way to properly notify observers when the title/content of an Article has changed, a new Article has been added, and an Article has been removed?
It seems the notifications are only possible when an entirely new collection is set on the LiveData, which would seem to result in a really inefficient UI refresh.
Hypothetical Example
Suppose the following...
My LiveData class looks something like this:
public class ArticleViewModel extends ViewModel {
private final MutableLiveData<List<Article>> mArticles = new MutableLiveData<>();
}
I want to display the Articles in a list by using the RecyclerView. So any time my Fragment observes a change in the ArticleViewModel's LiveData it calls the following method on my custom RecyclerView.Adapter class:
public class ArticleRecyclerViewAdapater extends RecyclerView.Adapter<Article> {
private final ArrayList<Article> mValues = new ArrayList<>();
public void resetValues(Collection<Article> articles) {
mValues.clear();
mValues.addAll(articles);
notifyDataSetChanged();
}
}
Finally, my application will allow the user to add a new Article, delete an existing Article, and change an existing Article's name (which needs to be updated in the RecyclerView list). How can I do that properly?
Add/Remove Article
It seems the LiveData construct doesn't notify observers if you add/remove an item from the underlying Collection. It seems you'd have to call LiveData#setValue, perhaps the ArticleViewModel would need a method that looks something like this:
public void deleteArticle(int index) {
final List<Article> articles = mArticles.getValue();
articles.remove(index);
mArticles.setValue(articles);
}
Isn't that really inefficient because it would trigger a complete refresh in the RecyclerView.Adapter as opposed to just adding/removing a single row?
Change Name
It seems the LiveData construct doesn't notify observers if you change the contents of an item in the underlying collection. So if I wanted to change the title of an existing Article and have that reflected in the RecyclerView then my ArticleViewModel would have to modify the object and call LiveData#setValue with the entire collection.
Again, isn't this really inefficient because it would trigger a complete refresh in the RecyclerView.Adapter?
Case1:
When you add or delete
So when you add or delete the element in the list you don't change the refernce of list item so every time you modify the liveData item you have to update live data value by calling setValue method(if you are updating the item on main thread)or Post value(when you are updating the value on background thread)
The problem is that it is not efficient
Solution
Use diffutil
Case 2:When you are updating the item property by editing the item.
The Problem
LiveData will only alert its observers of a change if the top level value has changed. In the case of a complex object, though, that means only when the object reference has changed, but not when some property of the object has changed.
Solution
To observe the change in property you need PropertyAwareMutableLiveData
class PropertyAwareMutableLiveData<T: BaseObservable>: MutableLiveData<T>() {
private val callback = object: Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
value = value
}
}
override fun setValue(value: T?) {
super.setValue(value)
value?.addOnPropertyChangedCallback(callback)
}
}
Two things to note here:
1.First is that our value is a generic type which implements the BaseObservable interface. This gives us access to the OnPropertyChangedCallback.
2.Next is that, whenever some property of the BaseObservable changes, we simply reset the top level value property to be its current value, thus alerting the observers of the LiveData that something has changed.
LiveData will only notify when its wrapped object reference is changed. When you assign a new List to a LiveData then it will notify because its wrapped object reference is changed but if add/remove items from a LiveData's List it will not notify because it still has the same List reference as wrapped object. So you can overcome this problem by making an extension of MutableLiveData as explained here in another stackoverflow question.
I know it's too late to answer.
But I hope it will help other developers searching for a resolution on a similar issue.
Take a look at LiveAdapter.
You just need to add the latest dependency in Gradle.
dependencies {
implementation 'com.github.RaviKoradiya:LiveAdapter:1.3.4'
// kapt 'com.android.databinding:compiler:GRADLE_PLUGIN_VERSION' // this line only for Kotlin projects
}
and bind adapter with your RecyclerView
// Kotlin sample
LiveAdapter(
data = liveListOfItems,
lifecycleOwner = this#MainActivity,
variable = BR.item )
.map<Header, ItemHeaderBinding>(R.layout.item_header) {
onBind{
}
onClick{
}
areContentsTheSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.map<Point, ItemPointBinding>(R.layout.item_point) {
onBind{
}
onClick{
}
areContentsTheSame { old: Point, new: Point ->
return#areContentsTheSame old.id == new.id
}
areItemSame { old: Header, new: Header ->
return#areContentsTheSame old.text == new.text
}
}
.into(recyclerview)
That's it. Not need to write extra code for adapter implementation, observe LiveData and notify the adapter.

Categories

Resources