Reputation: 4719
I'm using the new support library ListAdapter
. Here's my code for the adapter
class ArtistsAdapter : ListAdapter<Artist, ArtistsAdapter.ViewHolder>(ArtistsDiff()) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(parent.inflate(R.layout.item_artist))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getItem(position))
}
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(artist: Artist) {
itemView.artistDetails.text = artist.artistAlbums
.plus(" Albums")
.plus(" \u2022 ")
.plus(artist.artistTracks)
.plus(" Tracks")
itemView.artistName.text = artist.artistCover
itemView.artistCoverImage.loadURL(artist.artistCover)
}
}
}
I'm updating the adapter with
musicViewModel.getAllArtists().observe(this, Observer {
it?.let {
artistAdapter.submitList(it)
}
})
My diff class
class ArtistsDiff : DiffUtil.ItemCallback<Artist>() {
override fun areItemsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem?.artistId == newItem?.artistId
}
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
}
What's happening is when submitList is called the first time the adapter renders all the items, but when submitList is called again with updated object properties it does not re-render the view which has changed.
It re-renders the view as I scroll the list, which in turn calls bindView()
Also, I've noticed that calling adapter.notifyDatasSetChanged()
after submit list renders the view with updated values, but I don't want to call notifyDataSetChanged()
because the list adapter has diff utils built-in
Can anyone help me here?
Upvotes: 168
Views: 90272
Reputation: 561
Because the problem lays inside the ListAdapter
, I would like to solve it inside the ListAdapter
.
Thanks to Kotlin extension, we can write it like:
class MyItemAdapter() :
ListAdapter<Item, RecyclerView.ViewHolder>(ItemDiffCallback) {
// ...
override fun submitList(list: List<Item>?) {
super.submitList(list?.toList())
}
}
It does look like a tricky hack. So I'd like to make a comment too:
super.submitList(list?.toList()) // to make submitList work, new value MUST be a new list. https://stackoverflow.com/a/50031492/9735961
And yes, thank you, RecyclerView developers.
Upvotes: 1
Reputation: 769
The adapter can not understand that you have some updates, I don't know why!? I am adding some entities to the list ad I m expected to collect them at the consumption point. But, nothing happens. As a solution that worked for me you can use the script below:
artistAdapter.submitList(it.toMutableList())
Upvotes: 0
Reputation: 13667
This is something naturally expecte to be available on the official API, but as it isn't, this can be a way to deal with it:
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.clearItems() {
submitList(null)
submitList(emptyList())
}
Upvotes: 0
Reputation: 102
In my case i was using same object(from adadptar) to update Room database. Create new object to update database and it'll fix the issue.
Example: I was doing this ->
val playlist = adapter.getItem(position)
playlist.name = "new name"
updatePlaylistObjectInRoomDatabase(playlist)
above code will change object in adapter before room database. So no change will be detected by DiffUtil callback
.
Now doing this ->
val playlist = adapter.getItem(position)
val newPlaylist = Playlist()
newPlaylist.id = playlist.id
newPlaylist.name = "new name"
updatePlaylistObjectInRoomDatabase(newPlaylist)
Above code will not change anything in adapter list and will only change data in room database. so submitList
will have different values DiffUtil callback
can detect.
Enjoy the little things :)
Upvotes: 0
Reputation: 518
Had a VERY similar issue, to this one, and decided to open a new thread and even create a GitHub project to mess around with. Most solutions didn't quite work for me, not even the toMutableList()
way. In my case, the problem was solved by using immutable classes and submitting immutable List
s to the Adapter.
Upvotes: 2
Reputation: 69
I also ran into similar issue, my usecase was i had a clickHandler and item will be selected/not selected (toggle on click).
I tried most of the approach from the above answers, only thing that worked is
adapter.submitList(null)
adapter.submitList(modifiedList)
but problem with this is everytime i click on any clickHandler the whole list is being redrawn again which is very ineffecient.
What i did ?
I made a live data that will store last clicked item and observing that live data, we can tell adapter that live data has been updated like below
viewModel.lastClicked.observe(viewLifeCycleOwner, {
adapter.notifyItemChanged(it)
}
Upvotes: 2
Reputation: 5085
Once you have modify the array list, you have to let adapter know that which position that should be change
this code below is working in my case wish it may help
private fun addItem() {
val index = myArrayList.size
val position = myArrayList.size+1
myArrayList.add(
index, MyArrayClass("1", "Item Name")
)
myAdapter.notifyItemInserted(position) // in case of insert
// in case of remove item
// val index = myArrayList.size-1
// myAdapter.notifyItemRemoved(index)
}
Upvotes: 0
Reputation: 438
this will work .... what happen Is when you get the current list you are pointing to the same list at same location
Upvotes: 1
Reputation: 907
I encounter a very similar issue.
After the data list changed, I submit it again, the recycler view doesn't show as I wanted. It shows duplicated items.
I haven't found the root cause, but I find a workaround, that is to set the adapter to recycler view again. I guess this makes recycler viewer forget the memory before and render again correctly.
userNftListFiltered = SOME_NEW_VALUE
binding.nftSendSearchList.adapter = searchNftAdapter //set adapter again
searchNftAdapter.submitList(userNftListFiltered)
Upvotes: 0
Reputation: 31
It solve my problem. I think the best way is not to override submitList
but add a new function to add new list.
fun updateList(list: MutableList<ScaleDispBlock>?) {
list?.let {
val newList = ArrayList<ScaleDispBlock>(list)
submitList(newList)
}
}
Upvotes: 3
Reputation: 515
The way that worked for me is to override the submitList()
and create a copy of the incoming list and each item inside it too:
override fun submitList(list: List<Item>?) {
val listCopy =
mutableListOf<Item>().apply {
list?.map {
add(Item(it.id, it.name, it.imageUrl))
}
}
super.submitList(listCopy)
}
Upvotes: 1
Reputation: 951
As has already been mentioned, you cannot submit a List with the same reference because the ListAdapter will see the lists are in the same location and will therefore not be able to use the DiffUtil.
The simplest solution would be to make a shallow copy of the list.
submitList(ArrayList(list))
Be wary converting the List to a MutableList, as that can create conditions for Exceptions and hard to find bugs.
Upvotes: 1
Reputation: 3109
I got some strange behavior. I'm using MutableList in LiveDate.
In kotlin, the following codes don't work:
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it)
})
But, when I change it to it.toList(), it works
mViewModel.products.observe(viewLifecycleOwner, {
mAdapter.submitList(it.toList())
})
Although, "it" was the same list.
Upvotes: 6
Reputation: 477
Optimal Soltion: for Kotlin
var list :ArrayList<BaseModel> = ArrayList(adapter.currentList)
list.add(Item("Content"))
adapter.submitList(list) {
Log.e("ListAdaptor","List Updated Successfully")
}
We should not maintain another base list as adapter.currentList will return a list in which diff is already calculated.
We have to provide a new instance every time a list updated because of DiffUtil As per android documentation DiffUtil is a utility class that calculates the difference between two lists and outputs a list of update operations that converts the first list into the second one. One list is already maintained by AsyncListDiffer which runs the diffutil on the background thread and another one has to be passed using adaptor.submitList()
Upvotes: 0
Reputation: 1529
I had a similar problem but the incorrect rendering was caused by a combination of setHasFixedSize(true)
and android:layout_height="wrap_content"
. For the first time, the adapter was supplied with an empty list so the height never got updated and was 0
. Anyway, this resolved my issue. Someone else might have the same problem and will think it is problem with the adapter.
Upvotes: 31
Reputation: 141
Wasted so much time to figure out the problem in same case.
But in my situation the problem was that i forgot to specify a layoutManager for my recyclerView: vRecyclerView.layoutManager = LinearLayoutManager(requireContext())
I hope no one will repeat my mistake...
Upvotes: 14
Reputation: 41
The reason your ListAdapter .submitlist is not called is because the object you updated still holds the same adress in memory.
When you update an object with lets say .setText it changes the value in the original object.
So that when you check if object.id == object2.id it will return as the same because the both have a reference to the same location in memory.
The solution is to create a new object with the updated data and insert that in your list. Then submitList will be called and it will work correctly
Upvotes: 3
Reputation: 3628
The library assumes you are using Room or any other ORM which offers a new async list every time it gets updated, so just calling submitList on it will work, and for sloppy developers, it prevents doing the calculations twice if the same list is called.
The accepted answer is correct, it offers the explanation but not the solution.
What you can do in case you're not using any such libraries is:
submitList(null);
submitList(myList);
Another solution would be to override submitList (which doesn't cause that quick blink) as such:
@Override
public void submitList(final List<Author> list) {
super.submitList(list != null ? new ArrayList<>(list) : null);
}
Or with Kotlin code:
override fun submitList(list: List<CatItem>?) {
super.submitList(list?.let { ArrayList(it) })
}
Questionable logic but works perfectly. My preferred method is the second one because it doesn't cause each row to get an onBind call.
Upvotes: 120
Reputation: 283
If you encounter some issues when using
recycler_view.setHasFixedSize(true)
you should definitly check this comment: https://github.com/thoughtbot/expandable-recycler-view/issues/53#issuecomment-362991531
It solved the issue on my side.
(Here is a screenshot of the comment as requested)
Upvotes: 17
Reputation: 41
I had a similar problem. The issue was in the Diff
functions, which didn't adequately compare the items. Anyone with this issue, make sure your Diff
functions (and by extension your data object classes) contain proper comparison definitions - i.e. comparing all fields which might be updated in the new item. For example in the original post
override fun areContentsTheSame(oldItem: Artist?, newItem: Artist?): Boolean {
return oldItem == newItem
}
This function (potentially) does not do what it says on the label: it does not compare the contents of the two items - unless you have overridden the equals()
function in the Artist
class. In my case, I had not, and the definition of areContentsTheSame
only checked one of the necessary fields, due to my oversight when implementing it. This is structural equality vs. referential equality, you can find more about it here
Upvotes: 4
Reputation: 3190
Using @RJFares first answer updates the list successfully, but doesn't maintain the scroll state. The entire RecyclerView
starts from 0th position. As a workaround, this is what I did:
fun updateDataList(newList:List<String>){ //new list from DB or Network
val tempList = dataList.toMutableList() // dataList is the old list
tempList.addAll(newList)
listAdapter.submitList(tempList) // Recyclerview Adapter Instance
dataList = tempList
}
This way, I'm able to maintain the scroll state of RecyclerView
along with modified data.
Upvotes: 0
Reputation: 2291
For anyone who's scenario is same as mine, I leave my solution, which I don't know why it's working, here.
The solution which worked for me was from @Mina Samir, which is submitting the list as a mutable list.
My Issue scenario :
-Loading a friend list inside a fragment.
ActivityMain attaches the FragmentFriendList(Observes to the livedata of friend db items) and on the same time, requests a http request to the server to get all of my friend list.
Update or insert the items from the http server.
Every change ignites the onChanged callback of the livedata. But, when it's my first time launching the application, which means that there was nothing on my table, the submitList succeeds without any error of any kind, but nothing appears on the screen.
However, when it's my second time launching the application, data are being loaded to the screen.
The solution is, as metioned above, submitting the list as a mutableList.
Upvotes: 1
Reputation: 12067
In my case I forgot to set the LayoutManager
for the RecyclerView
. The effect of that is the same as described above.
Upvotes: 7
Reputation: 2528
I needed to modify my DiffUtils
override fun areContentsTheSame(oldItem: Vehicle, newItem: Vehicle): Boolean {
To actually return whether the contents are new, not just compare the id of the model.
Upvotes: 0
Reputation: 115
For me, this issue appeared if I was using RecyclerView
inside of ScrollView
with nestedScrollingEnabled="false"
and RV height set to wrap_content
.
The adapter updated properly and the bind function was called, but the items were not shown - the RecyclerView
was stuck at its' original size.
Changing ScrollView
to NestedScrollView
fixed the issue.
Upvotes: 2
Reputation: 1609
with Kotlin just you need to convert your list to new MutableList like this or another type of list according to your usage
.observe(this, Observer {
adapter.submitList(it?.toMutableList())
})
Upvotes: 48
Reputation: 960
Today I also stumbled upon this "problem". With the help of insa_c's answer and RJFares's solution I made myself a Kotlin extension function:
/**
* Update the [RecyclerView]'s [ListAdapter] with the provided list of items.
*
* Originally, [ListAdapter] will not update the view if the provided list is the same as
* currently loaded one. This is by design as otherwise the provided DiffUtil.ItemCallback<T>
* could never work - the [ListAdapter] must have the previous list if items to compare new
* ones to using provided diff callback.
* However, it's very convenient to call [ListAdapter.submitList] with the same list and expect
* the view to be updated. This extension function handles this case by making a copy of the
* list if the provided list is the same instance as currently loaded one.
*
* For more info see 'RJFares' and 'insa_c' answers on
* https://stackoverflow.com/questions/49726385/listadapter-not-updating-item-in-reyclerview
*/
fun <T, VH : RecyclerView.ViewHolder> ListAdapter<T, VH>.updateList(list: List<T>?) {
// ListAdapter<>.submitList() contains (stripped):
// if (newList == mList) {
// // nothing to do
// return;
// }
this.submitList(if (list == this.currentList) list.toList() else list)
}
which can then be used anywhere, e.g.:
viewModel.foundDevices.observe(this, Observer {
binding.recyclerViewDevices.adapter.updateList(it)
})
and it only (and always) copies the list if it is the same as currently loaded one.
Upvotes: 8
Reputation: 1420
According to the official docs :
Whenever you call submitList it submits a new list to be diffed and displayed.
This is why whenever you call submitList on the previous (already submitted list), it does not calculate the Diff and does not notify the adapter for change in the dataset.
Upvotes: 11
Reputation: 2931
Edit: I understand why this happens that wasn't my point. My point is that it at least needs to give a warning or call the notifyDataSetChanged()
function. Because apparently I am calling the submitList(...)
function for a reason. I am pretty sure people are trying to figure out what went wrong for hours until they figure out the submitList() ignores silently the call.
This is because of Google
s weird logic. So if you pass the same list to the adapter it does not even call the DiffUtil
.
public void submitList(final List<T> newList) {
if (newList == mList) {
// nothing to do
return;
}
....
}
I really don't understand the whole point of this ListAdapter
if it can't handle changes on the same list. If you want to change the items on the list you pass to the ListAdapter
and see the changes then either you need to create a deep copy of the list or you need to use regular RecyclerView
with your own DiffUtill
class.
Upvotes: 200