NotifyDataSetChanged does not update the RecyclerView correctly - android

I am trying to implement a fairly basic logic within my recyclerview adapter but notifyDataSetChanged() is giving me quite the headache.
I have a filter method that looks like this:
fun filter(category: Int) {
Thread(Runnable {
activeFiltered!!.clear()
if (category == -1) {
filterAll()
} else {
filterCategory(category)
}
(mContext as Activity).runOnUiThread {
notifyDataSetChanged()
}
}).start()
}
where filterAll() and filterCategory() functions are quite easy:
private fun filterAll() {
activeFiltered?.addAll(tempList!!)
}
private fun filterCategory(category: Int) {
for (sub in tempList!!) {
if (sub.category == category) {
activeFiltered?.add(sub)
}
}
}
When I run this code and filter the list by category the activeFiltered list is updated correctly and contains the items I expect, but when notifyDataSetChanged() is run it only cuts the list's range without updating the items.
Is there a way to fix this?
I also tried, instead of notifyDataSetChanged() to use:
activeFiltered!!.forEachIndexed {index, _ -> notifyItemChanged(index)}
but the problem is still there.
It isn't a threading issue either since I tried putting the whole logic in the main thread and the list still wasn't updated correctly.
This is my onBindViewHolder():
override fun onBindViewHolder(viewHolder: ActiveViewHolder, pos: Int) {
sub = activeFiltered!![pos]
inflateView()
}
This is where I inflate my text, sub is the instance variable set in the onBindViewHolder():
private fun inflateView() {
viewHolder.title.text = sub.title
}

It seems the implementation of onBindViewHolder() is incorrect. In order to update a list item, the passed in viewHolder parameter should be used (not the viewHolder you created in the onCreateViewHolder()).
The correct implementation should be like
override fun onBindViewHolder(viewHolder: ActiveViewHolder, pos: Int) {
val sub = activeFiltered!![pos]
inflateView(viewHolder, sub)
}
private fun inflateView(viewHolder: ActiveViewHolder, sub: <YourDataType>) {
viewHolder.title.text = sub.title
}
By the way, it is not a good practice to hold something as a member field in order to access it in several methods. Feel free to pass it as arguments to such methods. In the above code I passed the sub as argument and not stored it as a member.
And also it is not necessary to hold the viewHolder that you create in onCreateViewHolder(). We mostly need them in some callback methods (like onBindViewHolder(), etc) and these methods will receive the right viewHolder as arguments.

I think you are using the original array in onBindView() instead of the filtered one.

Related

How does the recyclerview adapter know to call onCreateViewHolder?

I just saw this example class for an Adapter for Recyclerview and I'm a bit confused on how it knows to call onCreateViewHolder, onBindViewHolder, etc just from adding an Item object to a list?
Does it have something to do with the line notifyItemInserted(items.size - 1) ?
Is it that whenever this method is called the onCreateViewHolder method is recalled with for that item or?
Adapter:
class ListAdapter (
private val items: MutableList<Item>
) : RecyclerView.Adapter <ListAdapter.ListViewHolder>() {
class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return ListViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.list_items, parent, false)
)
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val currItem = items[position]
holder.itemView.apply {
tv_item.text = currItem.title
cb_item.isChecked = currItem.checked
crossItem(tv_item, currItem.checked)
cb_item.setOnCheckedChangeListener { _, isChecked ->
crossItem(tv_item, isChecked)
currItem.checked = !currItem.checked
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
}
}
}
override fun getItemCount(): Int {
return items.size
}
private fun crossItem (itemText: TextView, checked: Boolean) {
if (checked){
//dk wtf paint flags is
itemText.paintFlags = itemText.paintFlags or STRIKE_THRU_TEXT_FLAG
}
//else remove
else {
itemText.paintFlags = itemText.paintFlags and STRIKE_THRU_TEXT_FLAG.inv()
}
}
fun addItem (item: Item){
items.add (item)
notifyItemInserted(items.size - 1)
}
}
Item Class:
data class Item (
val title: String,
var checked: Boolean = false
)
{
}
Whenever the Adapter needs to provide a new view for the RecyclerView to draw, it checks if it has an unused ViewHolder in its pool. If it doesn't, it calls onCreateViewHolder() so it can create one. Then it calls onBindViewHolder() for the ViewHolder that came from either source so the contained view can be prepared before being added to the layout.
If you call one of the notify methods, that triggers it to refresh whichever item rows are affected. It will return any removed rows to the ViewHolder pool and then follow the above steps to get the views it needs for new rows. If you use a notify...changed method, it will only need to use onBindViewHolder() for the applicable rows. When you use the nuclear option notifyDataSetChanged(), it returns all items to the pool.
When the RecyclerView is first displayed, or when the layout is resized, those actions will possibly trigger the need to show more rows. When you scroll the list, items that scroll off the screen are returned to the ViewHolder pool, and when new items scroll into view, ViewHolders need to be created or acquired from the pool as explained above.
By the way, this is going to look ugly because it refreshes the whole list even though only some items are removed:
items.removeAll { item ->
item.checked
}
notifyDataSetChanged()
I recommend this instead so you get a nice transition:
for (i in items.indices.reversed()) {
if (items[i].checked) {
items.removeAt(i)
notifyItemRemoved(i)
}
}
I iterate in reverse so the indices that are removed are stable as you iterate and remove items.
override fun getItemCount(): Int {
return items.size
}
This function is the key, it knows how many to create and how many to bind by knowing how many there are in total. The amount of ViewHolders created is more based on how many Views can fit on the screen at one time.
This gets more complex when you have different view types, as it will sometimes has to create more ViewHolders than what was required from the start as view types change.
The notify... functions just let the Adapter know it needs to "re-look" at the List.

Kotlin ListAdapter reset RecyclerView after submitList

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.

ListAdapter Diff does not dispatch updates on same list instance, but neither on different list from LiveData

it is a known issue that ListAdapter (actually the AsyncListDiffer from its implementation) does not update the list if the new list only has modified items but has the same instance. The updates do not work on new instance list either if you use the same objects inside.
For all of this to work, you have to create a hard copy of the entire list and objects inside.
Easiest way to achieve this:
items.toMutableList().map { it.copy() }
But I am facing a rather weird issue. I have a parse function in my ViewModel that finally posts the items.toMutableList().map { it.copy() } to the LiveData and gets observes in the fragment. Even with the hard copy, DiffUtil does not work. If I move the hard copy inside the fragment, then it works.
To get this easier, if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items)
... then, it doesn't work. But if I do this:
IN VIEW MODEL:
[ ... ] parse stuff here
restaurants.postValue(items)
IN FRAGMENT:
restaurants.observe(viewLifecycleOwner, Observer { items ->
adapter.submitList(items.toMutableList().map { it.copy() })
... then it works.
Can anybody explain why this doesn't work?
In the mean time, I have opened an issue on the Google Issue Tracker because maybe they will fix the AsyncListDiffer not updating same instance lists or items. It defeats the purpose of the new adapter. The AsyncListDiffer SHOULD ALWAYS accept same instance lists or items, and fully update using the diff logic that the user customises in the adapter.
I made a quick sample using DiffUtil.Callback and ListAdapter<T, K> (so I called submitList(...) on the adapter), and had no issues.
Then I modified the adapter to be a normal RecyclerView.Adapter and constructed an AsyncDiffUtil inside of it (using the same DiffUtil.Callback from above).
The architecture is:
Activity -> Fragment (contains RecyclerView).
Adapter
ViewModel
"Fake Repository" that simply holds a val source: MutableList<Thing> = mutableListOf()
Model
I've created a Thing object: data class Thing(val name: String = "", val age: Int = 0).
For readability I added typealias Things = List<Thing> (less typing). ;)
Repository
It's fake in the sense that items are created like:
private fun makeThings(total: Int = 20): List<Thing> {
val things: MutableList<Thing> = mutableListOf()
for (i in 1..total) {
things.add(Thing("Name: $i", age = i + 18))
}
return things
}
But the "source" is a mutableList of (the typealias).
The other thing the repo can do is "simulate" a modification on a random item. I simply create a new data class instance, since it's obviously all immutable data types (as they should be). Remember this is just simulating a real change that may have come from an API or DB.
fun modifyItemAt(pos: Int = 0) {
if (source.isEmpty() || source.size <= pos) return
val thing = source[pos]
val newAge = thing.age + 1
val newThing = Thing("Name: $newAge", newAge)
source.removeAt(pos)
source.add(pos, newThing)
}
ViewModel
Nothing fancy here, it talks and holds the reference to the ThingsRepository, and exposes a LiveData:
private val _state = MutableLiveData<ThingsState>(ThingsState.Empty)
val state: LiveData<ThingsState> = _state
And the "state" is:
sealed class ThingsState {
object Empty : ThingsState()
object Loading : ThingsState()
data class Loaded(val things: Things) : ThingsState()
}
The viewModel has two public methods (Aside from the val state):
fun fetchData() {
viewModelScope.launch(Dispatchers.IO) {
_state.postValue(ThingsState.Loaded(repository.fetchAllTheThings()))
}
}
fun modifyData(atPosition: Int) {
repository.modifyItemAt(atPosition)
fetchData()
}
Nothing special, just a way to modify a random item by position (remember this is just a quick hack to test it).
So FetchData, launches the async code in IO to "fetch" (in reality, if the list is there, the cached list is returned, only the 1st time the data is "made" in the repo).
Modify data is simpler, calls modify on the repo and fetch data to post the new value.
Adapter
Lots of boilerplate... but as discussed, it's just an Adapter:
class ThingAdapter(private val itemClickCallback: ThingClickCallback) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
The ThingClickCallback is just:
interface ThingClickCallback {
fun onThingClicked(atPosition: Int)
}
This Adapter now has an AsyncDiffer...
private val differ = AsyncListDiffer(this, DiffUtilCallback())
this in this context is the actual adapter (needed by the differ) and DiffUtilCallback is just a DiffUtil.Callback implementation:
internal class DiffUtilCallback : DiffUtil.ItemCallback<Thing>() {
override fun areItemsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.name == newItem.name
}
override fun areContentsTheSame(oldItem: Thing, newItem: Thing): Boolean {
return oldItem.age == newItem.age && oldItem.name == oldItem.name
}
nothing special here.
The only special methods in the adapter (aside from onCreateViewHolder and onBindViewHolder) are these:
fun submitList(list: Things) {
differ.submitList(list)
}
override fun getItemCount(): Int = differ.currentList.size
private fun getItem(position: Int) = differ.currentList[position]
So we ask the differ to do these for us and expose the public method submitList to emulate a listAdapter#submitList(...), except we delegate to the differ.
Because you may be wondering, here's the ViewHolder:
internal class ViewHolder(itemView: View, private val callback: ThingClickCallback) :
RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.thingName)
private val age: TextView = itemView.findViewById(R.id.thingAge)
fun bind(data: Thing) {
title.text = data.name
age.text = data.age.toString()
itemView.setOnClickListener { callback.onThingClicked(adapterPosition) }
}
}
Don't be too harsh, I know i passed the click listener directly, I only had about 1 hour to do all this, but nothing special, the layout it's just two text views (age and name) and we set the whole row clickable to pass the position to the callback. Nothing special here either.
Last but not least, the Fragment.
Fragment
class ThingListFragment : Fragment() {
private lateinit var viewModel: ThingsViewModel
private var binding: ThingsListFragmentBinding? = null
private val adapter = ThingAdapter(object : ThingClickCallback {
override fun onThingClicked(atPosition: Int) {
viewModel.modifyData(atPosition)
}
})
...
It has 3 member variables. The ViewModel, the Binding (I used ViewBinding why not it's just 1 liner in gradle), and the Adapter (which takes the Click listener in the ctor for convenience).
In this impl., I simply call the viewmodel with "modify item at position (X)" where X = the position of the item clicked in the adapter. (I know this could be better abstracted but this is irrelevant here).
there's only two other implemented methods in this fragment...
onDestroy:
override fun onDestroy() {
super.onDestroy()
binding = null
}
(I wonder if Google will ever accept their mistake with Fragment's lifecycle that we still have to care for this).
Anyway, the other is unsurprisingly, onCreateView.
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.things_list_fragment, container, false)
binding = ThingsListFragmentBinding.bind(root)
viewModel = ViewModelProvider(this).get(ThingsViewModel::class.java)
viewModel.state.observe(viewLifecycleOwner) { state ->
when (state) {
is ThingsState.Empty -> adapter.submitList(emptyList())
is ThingsState.Loaded -> adapter.submitList(state.things)
is ThingsState.Loading -> doNothing // Show Loading? :)
}
}
binding?.thingsRecyclerView?.adapter = adapter
viewModel.fetchData()
return root
}
Bind the thing (root/binding), get the viewModel, observe the "state", set the adapter in the recyclerView, and call the viewModel to start fetching data.
That's all.
How does it work then?
The app starts, the fragment is created, subscribes to the VM state LiveData, and triggers the Fetch of data.
The ViewModel calls the repo, which is empty (new), so makeItems is called the list now has items and cached in the repo's "source" list. The viewModel receives this list asynchronously (in a coroutine) and posts the LiveData state.
The fragment receives the state and posts (submit) to the Adapter to finally show something.
When you "click" on an Item, ViewHolder (which has a click listener) triggers the "call back" towards the fragment which receives a position, this is then passed onto the Viewmodel and here the data is mutated in the Repo, which again, pushes the same list, but with a different reference on the clicked item that was modified. This causes the ViewModel to push a new LIveData state with the same list reference as before, towards the fragment, which -again- receives this, and does adapter.submitList(...).
The Adapter asynchronously calculates this and the UI updates.
It works, I can put all this in GitHub if you want to have fun, but my point is, while the concerns about the AsyncDiffer are valid (and may be or been true), this doesn't seem to be my (super limited) experience.
Are you using this differently?
When I tap on any row, the change is propagated from the Repository
UPDATE: forgot to include the doNothing function:
val doNothing: Unit
get() = Unit
I've used this for a while, I normally use it because it reads better than XXX -> {} to me. :)
While doing
items.toMutableList().map { it.copy() }
restaurants.postValue(items)
you are creating a new list but items remains the same. You have to store that new list into a variable or passing that operation directly as a param to postItem.

In Android Kotlin, what's the right way to pass a onclick event into a viewholder?

Is there any difference in these two ways?
I've been using the seond way and it works so far, yet I found the first way upon reading tutorial articles.
1st:
class FlowersAdapter(private val onClick: (Flower) -> Unit) :
ListAdapter<Flower, FlowersAdapter.FlowerViewHolder>(FlowerDiffCallback) {
/* ViewHolder for Flower, takes in the inflated view and the onClick behavior. */
class FlowerViewHolder(itemView: View, val onClick: (Flower) -> Unit) :
RecyclerView.ViewHolder(itemView) {
private val flowerTextView: TextView = itemView.findViewById(R.id.flower_text)
private val flowerImageView: ImageView = itemView.findViewById(R.id.flower_image)
private var currentFlower: Flower? = null
init {
itemView.setOnClickListener {
currentFlower?.let {
onClick(it)
}
}
}
/* Bind flower name and image. */
fun bind(flower: Flower) {
currentFlower = flower
flowerTextView.text = flower.name
if (flower.image != null) {
flowerImageView.setImageResource(flower.image)
} else {
flowerImageView.setImageResource(R.drawable.rose)
}
}
}
}
First way of writing
2nd:
class FlowerViewHolder(itemView: View) :
RecyclerView.ViewHolder(itemView) {
private val flowerTextView: TextView = itemView.findViewById(R.id.flower_text)
private val flowerImageView: ImageView = itemView.findViewById(R.id.flower_image)
private var currentFlower: Flower? = null
/* Bind flower name and image. */
fun bind(flower: Flower) {
currentFlower = flower
flowerTextView.text = flower.name
if (flower.image != null) {
flowerImageView.setImageResource(flower.image)
} else {
flowerImageView.setImageResource(R.drawable.rose)
}
itemView.setOnClickListener {
onClick(flower)
}
}
}
Second way of writing
Appreicate your time and effort in telling me the differences.
From the perceptive of separation of concern, all the clickListeners are supposed to be handled in the Activity or Fragment and Adapters are meant just to wrap around the items, in your case Flower and present them in a way which can be used by the RecyclerView to display on the screen.
With that being said, the core logic of clickListeners is to be moved out of the bind method into the activity/fragment and that's precisely whats the firstMethod is all about. Matter of fact, I haven't noticed any performance improvement by employing the FirstMethod over the second one yet I insist on using FirstOne because its more of code organizing.
IMHO I feel like the adapter should know nothing about click listeners or any details about the ViewHolder; so I wouldn't pass the callback through the adapter.
I like passing the callback to my ViewHolder but instead of mapping into the init block I do it on the onBind hook from the adapter where I receive the view as a parameter. Also, I pass or update the ViewHolders directly into my Adapters. And then have some generic functions to compute whether the data-set has changed or not.
If you do it like this, you have the benefit that you may build 1 generic adapter and use it elsewhere without really minding how many different types of ViewHolder you may have to implement later on as it is completely agnostic.
TLDR;
So based on what you've provided us I would use the good things of both approaches. Binding the callback into the bind hook and passing the callback through the constructor of the ViewHolder

Add additional data to RecyclerView items

I have a functioning RecyclerView which works fine with the given data I provide via ListAdapter as shown below. What I now want is to add additional data to my list items.
class IngredientAdapter(
private val ingredientClickListener: IngredientClickListener
) : ListAdapter<Ingredient, RecyclerView.ViewHolder>(EventDiffCallback()) {
private val adapterScope = CoroutineScope(Dispatchers.Default)
fun submitIngredientsList(list: List<Ingredient>?) {
adapterScope.launch {
withContext(Dispatchers.Main) {
submitList(list)
}
}
}
I have no idea how to do that properly or if RecyclerViews are even capable of doing this. The only way I am able to see is merging both data classes (Ingredient plus the new one) together and submit them as list together to the adapter but this seems messy and I am looking for a better way.
So my question is: How to feed data into my list items without merging it together with the data I already have? Is RecyclerView the wrong choice in my case?
Thanks in advance!
Ok I found a solution: I submitted the additional data list just how like the other one but did not attach it directly to the ListAdapter since this is not possible.
In the function onBindViewHolder after getting the item for the current position I use this information to retrieve the correct element from the new data list. Then I attach the data to the view by calling using the viewholders view binding
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is ViewHolder -> {
val resources = holder.itemView.context.resources
val ingredientItem = getItem(position)
holder.bind(ingredientClickListener, ingredientItem)
val groceryInStock: GroceryInStock? = availableGroceries.firstOrNull{
ingredientItem.grocery.groceryId == it.grocery.groceryId
}
holder.binding.listItemAvailableAmount.text = groceryInStock.amount
}
}
}
Since the data I add fully depends on the already existing item being displayed I did not make any changes to the functions areItemsTheSame and areContentsTheSame in my overriden DiffUtil.ItemCallback class.

Categories

Resources