I have a RecyclerView that has a list of jobs. The jobs are downloaded and updated in the database. The changes is then reflected in the list and all is good so far.
The data is downloaded and stored regulary in a room database and populated with data binding and live objects.
To avoid that the list is refreshed when the data did not change (overwritten with the same content in the db) I have added a custom DiffCallback for the ListAdapter.
class JobDiffCallback : DiffUtil.ItemCallback<JobWrapper>() {
override fun areItemsTheSame(oldItem: JobWrapper, newItem: JobWrapper): Boolean {
return oldItem.job.jobId == newItem.job.jobId
}
override fun areContentsTheSame(oldItem: JobWrapper, newItem: JobWrapper): Boolean {
return oldItem == newItem
}
}
These makes sure that the objects in the list does not fade in and out when updated with the same content. And also makes sure that data changes are reflected in the list as expected.
The problem is now, that even though the items no longer fades in and out, they will still animate as if they were reordered and back again - so moving a bit.
My question is then: What am I missing to let the view know that the data actually just was updated with the same content and that it should not make any visual changes?
In areContentsTheSame you're supposed to check all relevant variables one by one.
oldItem == newItem is only checking the references (unless you are comparing data classes or have overridden the isEqual() method) of the objects which is not what you want. when you check the references, they are never the same so all items count as items with different content. you need to check the content in details yourself.
So do something like this:
override fun areContentsTheSame(oldItem: JobWrapper, newItem: JobWrapper): Boolean {
return oldItem.job.id == newItam.job.id && oldItem.job.name == newItem.job.name && ....
}
or change your classes to data classes.
Or override the isEqual() method in your job class and use the the method to compare them.
Related
I'm working on android apps using MVVM, and Data Binding. I'm using ListAdapter for my RecyclerView Adapter. The case is, when I submit new data to the adapter using submitList, it reset RecyclerView scroll position. It blink at first and just reset it's position to the top.
My Binding Adapter
#BindingAdapter("listTemplate", "hirarki")
fun bindListTemplate(recyclerView: RecyclerView, data: List<Template>?, hirarki: Int) {
var adapter = recyclerView.adapter as TemplateChiefAdapter
adapter.submitList(data)
}
TemplateFragment where I resubmit my data
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<Boolean>("shouldUpdate")
?.observe(viewLifecycleOwner, {
if (it) {
viewModel.fetchdata()
navController.currentBackStackEntry?.savedStateHandle?.remove<Boolean>("shouldUpdate")
}
})
This piece of code will update LiveData in my ViewModel, so the DataBinding will detect its change and re-submitList the data to the adapter
My List Adapter
class TemplateChiefAdapter(val onClickListener: OnClickListener) : ListAdapter<Template, TemplateChiefAdapter.TemplateChiefViewHolder>(DiffCallback) {
class TemplateChiefViewHolder(private var binding: ItemTemplateChiefBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(template: Template) {
binding.template = template
binding.executePendingBindings()
}
}
companion object DiffCallback : DiffUtil.ItemCallback<Template>() {
override fun areItemsTheSame(oldItem: Template, newItem: Template): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Template, newItem: Template): Boolean {
return oldItem.id_template == newItem.id_template
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TemplateChiefViewHolder {
return TemplateChiefViewHolder(ItemTemplateChiefBinding.inflate(LayoutInflater.from(parent.context)))
}
override fun onBindViewHolder(holder: TemplateChiefViewHolder, position: Int) {
val template = getItem(position)
holder.itemView.setOnClickListener {
onClickListener.onClick(template)
}
holder.bind(template)
}
class OnClickListener(val listener: (template: Template) -> Unit) {
fun onClick(template: Template) = listener(template)
}
}
How can I keep the recycler scroll position after submitList called?
I didn't examine in ultra detail all your code, but the DiffUtil Callback caught my attention.
areItemsTheSame is an optimization from the DiffUtil class to determine if the items changed position. If the didn't, then the contents can be checked, and re-bound to their new data if it changed. If the positions changed, then the item may need to be animated elsewhere or well.. as you can imagine it becomes more complicated from there.
The idea of that method is to compare if the items are the same or not, not to compare the entire item. I would use an id (or anything that can help you identify uniqueness in your items). You are using the === operator and I don't know the rest of your architecture, but comparing by reference may not be accurate if, for instance, your data layer transforms and copies these objects around (something you can't/shouldn't tell/care for in your adapter).
For instance, instead of
return oldItem === newItem
You could do
return oldItem.someId === newItem.someId
This would ensure that even if your items are the same but were copied/recreated/etc., you'd still identify them as such despite them being a different reference.
Then, in areContentsTheSame you are expected to check all the contents that you consider instrumental in deciding if onBind must be called on your specific viewHolder because the contents are different. So I would have expected something more like:
oldItem.something == newItem.something
&& oldItem.xxx == newItem.xxx
&& oldItem.yyy == newItem.yyy
(but maybe with DataBinding you don't need this, I wouldn't know).
All that being said, I have 0.1 experience with DataBinding (and personally for me that was enough), so if this is related in anyway how the data binding library behaves, I can't help you any more. :/
From a RecyclerView's point of view, the rest of the code looks adequate.
Hey I am new in DiffUtil in adpater. I read some articles from stack overflow, google docs and some articles. I am trying to understand callback of DiffUtil areItemsTheSame and areContentsTheSame but, I am not clear what that means. I am adding some code, please have a look. If I am doing wrong please guide me.
GroupKey
data class GroupKey(
val type: EnumType,
val sender: Sender? = null,
val close: String? = null
)
EnumType
enum class EnumType {
A,
B
}
Sender
data class Sender(
val company: RoleType? = null,
val id: String? = null
)
RoleType
data class RoleType(
val name : String?= null
val id: String? = null
)
Group
data class Group(
val key: GroupKey,
val value: MutableList<Item?>
)
I am passing my list to adapter which is a Group mutableList
var messageGroupList: MutableList<Group>? = null
..
val adapter = MainAdapter()
binding.recylerview.adapter = adapter
adapter.submitList(groupList)
Using DiffUtil in adapter
MainAdapter.kt
class MainAdapter :ListAdapter<Group, RecyclerView.ViewHolder>(COMPARATOR) {
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Group>() {
override fun areItemsTheSame(oldItem: Group, newItem: Group): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Group, newItem: Group): Boolean {
return ((oldItem.value == newItem.value) && (oldItem.key == newItem.key))
}
}
}
.....
}
1. Here do I need to compare key other property like type, sender etc. also inside this DiffUtil.ItemCallback.
2. when to use == or === and what about equals()
3. If we compare int, boolean or String we use == or something else ?
Inside this adapter I am calling another Recyclerview with passing list of Item inside that adapter.
Item
data class Item(
val text: String? = null,
var isRead: Boolean? = null,
val sender: Sender? = null,
val id: Int? = null
)
NestedRecyclerView.kt
class NestedRecyclerView : ListAdapter<Item, IncomingMessagesViewHolder>(COMPARATOR) {
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Item>() {
override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
return ((oldItem.isRead == oldItem.isRead) &&
(oldItem.sender == newItem.sender) &&
(oldItem.text == oldItem.text))
}
}
}
}
Again Same question Do I need to compare sender's other property here as well.
4. In areItemsTheSame do I need to compare id or just oldItem == newItem this?
5. How to proper way to update my adapter items. In normal reyclerview we use notifiyDataSetChanged. But in diffutil do I need to call again submitList function and it will take care of everything?
adapter.submitList(groupList)
Questions 1 and 4:
areItemsTheSame means that the two instances represent the same data item, even if some of the contents might be different. Suppose you had a list of contacts, and Jane's middle initial has been changed, but the row should still represent the same person Jane. There might be distinct instances of your model class, with some different values, but they are supposed to represent the same row.
So, usually you will compare only one thing between the old and new items that will be the same for each of them in this case. Usually, if you're getting data from a database or API, there will be some unique ID that represents a data point, and that's all you need to compare in areItemsTheSame. For example, oldItem.id == newItem.id.
areContentsTheSame means that if the two instances were each displayed in your list, they would look identical. So if you are using a data class, it is sufficient to use oldItem == newItem because a data class has an equals function that compares every property.
In your Item callback code, it looks like your areItemsTheSame is correct, but your areContentsTheSame is overly complex. Since Item is a data class, you only need to compare the two items directly.
override fun areContentsTheSame(oldItem: Item, newItem: Item) = oldItem == newItem
In your Group callback code, maybe you can compare the GroupKeys of the old and new items if that is a valid way to determine the items are the same. Since you are using only a direct == comparison, when items change partially you might have some visual defects like views disappearing and reappearing instead of simply having some of their text change.
Question 2
You should rarely ever have to use === in Kotlin. It does not only check if two items are equivalent, but it checks if the two items refer to the exact same instance in memory. It is not appropriate for DiffUtil.ItemCallback at all.
Question 3
== is the correct way to compare any two objects. In Kotlin, even primitives should be compared this way because they behave like objects.
Question 5
With ListAdapter, you should always use submitList instead of notifyDataSetChanged. notifyDataSetChanged would cause a pointless refresh of all the views and defeat the purpose of using ListAdapter and DiffUtil.
I have a recycler view whose adapter uses ListAdapter (version 1.1.0) :
class InnerEpisodeFragmentAdapter(
private val actCtx: Context,
) : ListAdapter<Episode, InnerEpisodeFragmentAdapter.MViewHolder>(COMPARATOR) {
...
The recycler view is fed by a kotlin flow coming from Room database Episode table :
vm.episodesFlow().asLiveData().observe(viewLifecycleOwner) { episodes ->
episodes.let { adapter.submitList(it) }
}
#Query("SELECT * FROM Episode ORDER BY pubDate DESC")
fun episodesFlow(): Flow<List<Episode>>
As excepted, each time a tuple changes in Episode table, a new list of episodes is emitted and the recycler view is update.
It works fine, but with an horrible BLINK at each update. It gives a bad user experience.
How can I avoid this blinking when the flow emit new values ?
In the previous version of my app, I used functions like notifyDataSetChanged() or notifyItemChanged() which never blink.I know I could still try to use these functions but I would be very disappointed if I could not avoid the blinks when using the kotlin flow as shown above. Thanks.
you comparators
areItemsThesame() is wrongly implemented. What you are comparing is reference where is old object and newObject might not same objects. Instead you some use some kind of uuid to compare the two like primary key. If objects are different it will return false and hence areContentsTheSame will never be called otherwise if objects are same their is no point of comparing contents as both oldObject and newObject are pointing to same reference.
Sorry for errors as I am writing from Mobile
======== REQUESTED ADDITIONAL INFORMATION ======
My comparator is fine and does exactly what I want.
For example, I want isPlayed to be updated in real time in each recycler view item. And it is. But with a blink of the whole list. The recycler view disappears for a fraction of a second then reappear with the updated info.
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<Episode>() {
override fun areItemsTheSame(oldItem: Episode, newItem: Episode): Boolean {
return oldItem === newItem
}
override fun areContentsTheSame(oldItem: Episode, newItem: Episode): Boolean {
return (oldItem.id == newItem.id)
&& (oldItem.isOnDisk == newItem.isOnDisk)
&& (oldItem.downloadId == newItem.downloadId)
&& (oldItem.isPlayed == newItem.isPlayed)
&& (oldItem.isDeleted == newItem.isDeleted)
}
}
}
I have a recycler view in my layout, at first it will be filled by data which is stored in local database, and then after a few second it will be updated using server.
the problem is when it updates, items of recycler view change suddenly, how can I set an animation for recycler view that change the items smoothly?
I notify my recycler view just like this:
fun add(list: List<BestStockModel>) {
items.clear()
items.addAll(list)
notifyItemRangeChanged(0, list.size)
}
There's a better way for you do so, you can use ListAdapter link.
Using ListAdapter you can simply submit a new list and the adapter will calculate the diff between the old one and the new one and add need animations for new/changed/deleted items.
It can detect the diff using simple callbacks that you provide to it.
Here's an example that you can use as a reference:
class HomeMoviesAdapter : ListAdapter<Movie, MoviesViewHolder>(
//note the following callbacks, ListAdapter uses them
// in order to find diff between the old and new items.
object : DiffUtil.ItemCallback<Movie>() {
override fun areItemsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem.title == newItem.title //this can be a unique ID for the item
override fun areContentsTheSame(oldItem: Movie, newItem: Movie): Boolean =
oldItem == newItem
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MoviesViewHolder {
val v: View = LayoutInflater.from(parent.context)
.inflate(R.layout.movies_item_view, parent, false)
return MoviesViewHolder(v)
}
override fun onBindViewHolder(holder: MoviesViewHolder, position: Int) {
//your binding logic goes here as usual.
}
}
And then from where you have the list (ex: fragment) you can do the following:
adapter.submit(newList)
And that's it for the list adapter to do the needed animations for you.
There's one gotcha though: if submitted the same list reference, the adapter will consider it the same as the old list, meaning it won't trigger the diff calculations. Note the following example:
//the following is a bad practice DO NOT do this!
val list: MutableList<Int> = mutableListOf(1, 2, 3)
adapter.submitList(list)
list.clear()
list.add(7)
adapter.submitList(list) //nothing will happen, since it's the same ref
Compare that to the following:
//the following is good practice, try to do the following!
adapter.submitList(listOf(1, 2, 3))
adapter.submitList(listOf(7)) //will delete all the old items, insert 7 and will also trigger the need animations correctly.
Although they both seem similar, they quite different: the second one submits a totally new list "reference-wise" to the adapter, which will cause the ListAdapter to trigger the calculations correctly.
I have a RecyclerView of images that the user is able to edit directly in the RecyclerView itself.
The editing part is fine and works well. It's done on a bitmap that overlays the images and then saves any changes to the image file. As soon as the user scrolls the recyclerview, the bitmap is destroyed and becomes invisible.
The trouble is, any changes the user makes aren't visible when they scroll. They scroll the OLD image, not the EDITED image. They have to get out of the recycler and back into it to see the changes.
So how do I force the RecyclerView to reload the image that was just saved. I'm using Glide in my adapter, and as you can see I have the caching based on the save time of the image.
class InfinityPageAdapter(val memo: Memo) : ListAdapter<Page, InfinityPageAdapter.ViewHolder>(PageDiffCallback()) {
class ViewHolder(pageView: View) : RecyclerView.ViewHolder(pageView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InfinityPageAdapter.ViewHolder {
val pageView = LayoutInflater.from(parent.context).inflate(R.layout.infinity_page_list_item, parent, false)
return ViewHolder(pageView)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val page = getItem(position)
val imageUrl = getMemoPath(memo.uuid) + page.uuid + ".webp"
if (imageUrl.isNotEmpty()) {
Glide.with(holder.itemView.context)
.load(imageUrl)
.apply(RequestOptions()
.signature(ObjectKey(System.currentTimeMillis()))
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.override(memo.pageWidth,memo.pageHeight)
)
.transition(DrawableTransitionOptions.withCrossFade())
.into(holder.itemView.findViewById(R.id.memo_page_image))
}
}
Is there a way to notify the recyclerview that the image has changed, and force a reload of the image?
I'm using DiffUtil on the adapter. My understanding is, I don't need to use notifyDataSetChanged() when you're using DiffUtil. But I don't understand how you notify of an image change.
I don't know whether the Diff Callback is the issue. For what it's worth, here it is. It doesn't look at the image file itself, just the name of the image file. It might be that, because the name isn't changing, the image doesn't update. But I don't want to be changing the name of the file with every edit.
class PageDiffCallback : DiffUtil.ItemCallback<Page>() {
override fun areItemsTheSame(oldItem: Page, newItem: Page): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: Page, newItem: Page): Boolean {
return oldItem == newItem
}
}
thanks!
John
This thing is, you are not updating an entire element in your recycler view, this is, a row. The notifyDataSetChanged() will force the data update like you said, but in this case it will also update your layout.
With DiffUtil, besides contents you are also comparing if the items are the same, which they are, since you are comparing their ID, and that isn't changing when you update your image.
Before trying with notifyDataSetChanged() tho, could you try to do this in your areContentsTheSame()method:
override fun areContentsTheSame(oldItem: Page, newItem: Page): Boolean {
return oldItem.id == newItem.id && oldItem.image == newItem.image // If you want just add more validations here
}