Can I use notifyDataSetChanged() in view Holder?
class ItemViewHolder(context: Context?, view: View) : RecyclerView.ViewHolder(view) {
func update(){
// ...
// i need to update adapter for example
adapter.notifyDataSetChanged()
}
}
Turning your class into an inner class is certainly one approach. I would also consider handing your ViewHolder a reference to a Listener or Callback interface that you come up with (or a set of functions) as well. This would make refactoring easier, if you ever decided to move your ViewHolder to a different file or outside of your adapter otherwise. It also better respects the Single Responsibility Principle, as we remove data-management from our ViewHolder and let that happen elsewhere.
For example:
class ViewHolder(
private itemView: View
private val onDataSetChanged: () -> Unit
) : RecyclerView.ViewHolder(itemView) {
fun onUpdate() {
// Perform changes...
onDataSetChanged()
}
}
It is a bit strange to me though that a ViewHolder would be manipulating data items like this. Generally, these only hold onto the item's positional information and view-state, not the item itself. Another approach might be to pass positional information to the update lambda:
class ViewHolder(
private itemView: View
private val onDataSetChanged: (Int) -> Unit
) : RecyclerView.ViewHolder(itemView) {
fun onShouldUpdate() {
if (adapterPosition == RecyclerView.NO_POSITION) return
onDataSetChanged(adapterPosition)
}
}
And then handle that update elsewhere. For example, in either the Adapter, or the ViewModel logic:
fun onCreateViewHolder(...) {
val viewHolder = ViewHolder(...) { /* do update */ }
}
This leaves us at a point where now the Adapter is responsible for both mapping data to views as well as managing that data's state... and that's not very clean.
Here's how I like to do it:
class Adapter(private val updateItem: (Item) -> Unit) {
fun onCreateViewHolder(...) {
return ViewHolder(...) { updateItem(items[it]) }
}
}
Where we handle data manipulations in a more reasonable place. Here:
The ViewHolder maps a click or update event to the position of the item it is holding
The Adapter maps a position to an actual data item
The Presentation object (such as an Architecture Components ViewModel) (whomever provides the lambda to the adapter) performs the manipulation. (More on this coming below)
In this case, your notifyDataSetChanged would be handled instead by the presenter then feeding back into the Adapter. Perhaps, for example, you have a method on Adapter called setData(items: List<Item>). When a new list of items is available, the presenter notifies the adapter, which sets the items:
// In your adapter
fun setData(newItems: List<Item>) {
val oldItems = items
items = newItems
notifyDataSetChanged()
// Alternatively, utilize DiffUtil
}
Your ViewModel could then expose an observable field, perhaps utilizing LiveData.
class MyViewModel : ViewModel() {
val items: LiveData<List<Item>>
fun updateItem(item: Item) { ... }
}
Your Activity, where you set up your adapter, could then observe changes to this:
val adapter = Adapter(viewModel::updateItem)
// Note you could probably use a method reference instead of a lambda.
viewModel.items.observe(this) { adapter.setItems(it) }
I hope that sort of helps to outline the approach. I could talk all day about this, so feel free to ask any questions about this. The idea here is that we're clearly separating our concerns (SRP), removing the need to hand the entire adapter to the ViewHolder (ISP), and leveraging lambdas to make sure we're not directly depending on things we don't need to be (DIP).
Implement a onclick event listener in your View Holder and then delete a view holder like this.
public void onClick(View v) {
int id = v.getId();
switch (id) {
case R.id.musicPlay:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setType("audio/*");
intent.setData(Uri.parse(mItem.getContentUri()));
context.startActivity(intent);
break;
case R.id.musicShare:
Toast.makeText(context, "Music is shared", Toast.LENGTH_SHORT).show();
break;
case R.id.musicDelete:
if (context.getContentResolver().delete(Uri.parse(mItem.getContentUri()), null, null)>0)
Toast.makeText(context, mItem.getTitle() + " is deleted", Toast.LENGTH_SHORT).show();
MediaScannerConnection.scanFile(context, new String[]{
mItem.getData()},
null, new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
mValues.remove(mItem);
notifyDataSetChanged();
}
});
break;
}
}
Related
I want to highlight the item when the action mode is active in the adapter class. I am able to do so but the highlight state is gone after scrolling. I have tried various solutions but I don't know understand why this is happening?
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {
class MyViewHolder extends RecyclerView.ViewHolder {
public void bind(Items viewHolder_item) {
itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
isSelectMode = true;
if (viewHolder_item.getIsSelect()){
itemView.setBackgroundColor(Color.TRANSPARENT);
item.get(position).setSelect(false);
selectedList.remove(item.get(position));
} else {
itemView.setBackgroundColor(Color.GRAY);
item.get(position).setSelect(true);
selectedList.add(item.get(position));
}
if (selectList.size() == 0){
isSelectMode = false;
}
return true;
}
});
itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (isSelectMode){
if (viewHolder_item.getIsSelect()){
itemView.setBackgroundColor(Color.TRANSPARENT);
item.get(position).setSelect(false);
selectedList.remove(item.get(position));
} else {
itemView.setBackgroundColor(Color.GRAY);
item.get(position).setSelect(true);
selectedList.add(item.get(position));
}
if (selectList.size() == 0){
isSelectMode = false;
}
}
}
});
}
No matter which solution is implemented, the result is always the same. The highlighted color is gone after scrolling. Any help would be appreciated Thanks.
I recommend that you take an alternative approach to solve your problem. In your Item object add a boolean field called public boolean isSelected=false then just set that field to true inside your items list using items.get(position).isSelected=true when an item is clicked and then use this field in order to evaluate which state each item should be in, instead of using your selectedList. you actually wont need this list anymore.
So now you just have to modify your on bindViewHolder as follows:
#Override
public void onBindViewHolder(#NonNull MyAdapter.MyViewHolder holder, int position) {
Items item=items.get(position);
if (!item.getIsSelect()){
holder.itemView.setBackgroundColor(Color.TRANSPARENT);
} else {
holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
}
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
isSelectMode = true;
if (item.getIsSelect()){
holder.itemView.setBackgroundColor(Color.TRANSPARENT);
items.get(position).setSelect(false);
selectedList.remove(items.get(position));
} else {
holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
items.get(position).setSelect(true);
selectedList.add(items.get(position));
}
if (selectList.size() == 0){
isSelectMode = false;
}
return true;
}
});
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (isSelectMode){
if (item.getIsSelect()){
holder.itemView.setBackgroundColor(Color.TRANSPARENT);
items.get(position).setSelect(false);
selectedList.remove(items.get(position));
} else {
holder.itemView.setBackgroundColor(activity.getResources().getColor(R.color.red));
items.get(position).setSelect(true);
}
if (selectList.size() == 0){
isSelectMode = false;
}
}
}
});
}
btw. you dont need to add isSelect to the constructor of Items as this should be an ephemeral session specific variable
there are a bunch of instances that can make contains not work as you would expect we would probably have to dive into your Item object to find a cause here is just one link Why is .contains returning false when I add a duplicate?
A few additions to the existing answer:
Treat your Adapter data as non-mutable. Do NOT mutate data inside the adapter. The Adapter needs to well... adapt data from a Model into a View (Holder). It keeps track of where each data belongs in terms of positioning, and that's pretty much all it needs to do (aside from inflating the views naturally).
If you supply a list of Thing, where isSelected is not part of the model, but rather part of your need to keep track of whether the user has selected it or not, then create a simple data class like
data class ThingSelection(val thing: Thing, val isSelected: Boolean)
And use that in your adapter instead of just a List<Thing>, use List<ThingSelection>.
Do not do what you do in the onBindViewHolder. All you should do there is
val item = getItem(position)
holder.bind(item)
It's the ViewHolder's job to set its properties, background color, etc. based on your business rules.
If your "item" is a ThingSelection then the viewHolder's bind method can do
if (item.isSelected) { ... }
Pass anything else you need to this method as well, bind(...) is whatever you need it to be.
What to do when the user changes the item selection?
You have a click listener, but don't mutate data, have your callback/listener pass what happened to the caller.
I imagine in your viewHolder bind method you'd do something like:
view.setOnClickListener {
externalListenerThatTheAdapterReceivedFromTheOutside.onThingClicked(thing, thing.isSelected)
}
It's this external listener's responsibility to:
Do what it takes (possibly pass it directly to a ViewModel so it work on this new event), to ensure the adapter receives the new data (now where the Thing.isSelected is correct given what the user did).
Ensure the new immutable list is supplied to the adapter so the adapter can compute its new data and update (hint: use ListAdapter<T, K> with a DiffUtil.ItemCallback<T>, it makes your life easier.
UPDATE: Data Class
data class ItemWithSelection(val item: Item, val isSelected: Boolean)
Then change your adapter to be:
class YourAdapter(): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) {
// You only need to override these
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YourViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.your_item_layout, parent, false) // or use ViewBinding, whatever.
return YourViewHolder(view) // you can pass more stuff if you need here, like a listener...
}
override fun onBindViewHolder(holder: YourViewHolder, position: Int) {
val item = getItem(position)
holder.bindData(item, item.isSelected) //Item is of type ItemWithSelection...
}
}
// Somewhere in your ViewHolder...
fun bindData(item: ItemWithSelection, isSelected: Boolean) {
// do what it needs, e.g.:
if (isSelected) { ... }
}
// You also need a DiffUtilCallback...
internal class DiffUtil.ItemCallback<ItemWithSelection>() {
internal class DiffUtilCallback : DiffUtil.ItemCallback<ItemWithSelection>() {
override fun areItemsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem.item.xxx == newItem.item.xxx
override fun areContentsTheSame(oldItem: ItemWithSelection, newItem: ItemWithSelection) = oldItem == newItem
}
With all that wired up...
In your ViewModel or "layer where you get the data" (most likely not the fragment/activity) provide a List<ItemWithSelection>...
How to construct this really depends on how you store what is selected.
But let's say you have two lists for the sake of simplicity:
val all: List<Item>
val selected: List<Item>
When it's time to produce a list for the UI, you could (note: I'm gonna assume you use Coroutines... you pick your own flavor of asynchronous/reactive programming:
class YourViewModel: ViewModel() {
fun getYourListWithSelections() {
viewModelScope.launch(Dispatchers.Computation) {//for e.g.
val tempSelection = mutableListOf<ItemWithSelection>()
all.mapTo(tempSelection) {
ItemWithSelection(item = it, isSelected = selection.contains(it))
}
// Assuming you have a MutableLiveData somewhere in this viewmodel...
_uiState.postValue(SomeSealedClass.ListAvailable(tempSelection))
}
}
This is naturally observed from the Fragment...
class YourFragment: Fragment() {
fun observe() {
viewModel.uiState.observe(viewLifecycleOwner, { viewState ->
when (viewState) {
is SomeSealedClass.ListAvailable -> adapter.submitList(viewState.items.toMutableList())//need to pass a new list or ListAdapter won't refresh the list.
else ... //exercise for the reader ;)
}
})
}
}
All that is really missing here are the boiler plate stuff.
How does the ViewHolder (which sets a click listener on the View pass the info back to the ViewModel?
Supply your adapter with a Listener of your choice:
interface ItemClickListener {
fun onItemClicked(val item: Item, val isSelected: Boolean)
}
Implement that and pass it to your adapter when you create it:
class YourAdapter(private val listener: ItemClickListener): ListAdapter<ItemWithSelection, YourViewHolder>(DiffUtilCallback()) {
And pass it in your fragment:
class YourFragment: Fragment() {
private val listener = object : ItemClickListener {
fun onItemClicked(val item: Item, val isSelected: Boolean) {
// pass it directly, it's the VM job to deal with this and produce a new list via LiveData.
viewModel.onItemClicked(item, isSelected)
}
}
val adapter = YourAdapter(listener)
Hope that helps.
from reading many website im aware that the optimal way handling onclick on recycerview was to set clicklistener not in OnBindViewHolder, its either in onCreateViewHolder(explained in here) or pass the clickListener in "bind" method(explained in here).
but one thing that really bother me, was how clickListener handled in activity / fragment
val adapter : MyAdapter(){
//click goes here
}
it may be not much but, honestly its not so so readable
i prefer to handle click listener not in constuctor, but with on separate method, meyabe like this
adapter.onItemClickListner = {
val adapter = MyAdapter()
//click more readable, yay!
}
but im not sure, if its will affect performance or not, or do i really need to stick with click on constructor?
I myself have used this method before.. Instead of passing the listener in the constructor, I have set it explicitly from the method call as you described. When I used that approach I had to make sure in the adapter that i checked for initialization of the listener and its null safety. So, you'll have to make sure before calling that it's absolutely present and initialized before calling the listener methods in the adapter.
I gradually moved to constructor as I didn't want to have such conscious checks all over my adapter before any click. (When there were like multiple clickable views.)
create a interface which has some function to handle your onClick.
Now you need to implement this interface in your activity class like this :-
class MainActivity : InterfaceAdapterOnClick (Here InterfaceAdapterOnClick is an interface which will have some functions to handle your onClick)
Let's suppose we have one function named onClickItem in our interface.
So now when you create your adapter like this adapter = MyAdapter() you can pass your interface with that. So it should look like :-
var adapter = MyAdapter(this) (this because you have already implemented interface in your mainActivity class) and your adapter should look like :-
class MyAdapter(onClickHandler: InterfaceAdapterOnClick) {}
So with this at the end you will have a seperate interface method in your mainActivity and your mainAcitvity should look like :-
class MainActivity : InterfaceAdapterOnClick {
// This will be your seperate onClick handler for your recyclerView item
override func onClickItem(// You can take some parameter as input here like `posistion` or `adapterItem` here ) {
}
}
IMHO, If your adapter needs that itemClickListener to function properly then you should pass it through its constructor and passing it using a setter function is not good since you might forgot to pass it at some point you forget to pass the listener and waste your time to check why clicking on items does not work! (It happened to me :D). If you are concerned about readability then you can use an interface and pass the implementation to adapter or use some features of Kotlin like named parameter or pass the callable reference of a function to make it more readable. For example something like this
class MyAdapter(onItemClick: (MyData) -> Unit) {
// ...
}
And initialize it like this
val adapter = MyAdapter(
onItemClick = { data ->
// ...
}
)
or pass the function like this
class MyActivity: AppCompatActivity() {
override fun onCreateView(/**/) {
// ...
val adapter = MyAdapter(
onItemClick = ::onItemClick
)
// or
// val adapter = MyAdapter(::onItemClick)
}
private fun onItemClick(data: MyData) {
}
}
you can simply pass an interface between the activity and the adapter class but the best method is by passing a function to your adapter class, like this
adapter = BasketRVA { basketEntity: BasketEntity, minus: Boolean -> changeQuantity(basketEntity, minus) }
private fun changeQuantity(basketEntity: BasketEntity, minus: Boolean) {
///your code
}
and in the adapter type this
class BasketRVA(private val clickListener: (BasketEntity, Boolean)->Unit): RecyclerView.Adapter<BasketRVA.BasketViewHolder>()
and in your viewHolder class and bind function
inner class BasketViewHolder(binding: BasketItemBinding): RecyclerView.ViewHolder(binding.root) {
#SuppressLint("SetTextI18n")
fun bind(basketEntity: BasketEntity?, clickListener: (BasketEntity, Boolean) -> Unit){
binding.tvProductName.text = basketEntity?.productName
in your binding function create a click listener for your view and pass the function you want to proceed
binding.imMinus.setOnClickListener {
val minus = true
clickListener(basketEntity!!, minus)
}
in your bind viewHolder
override fun onBindViewHolder(holder: BasketViewHolder, position: Int) {
BasketViewHolder(binding).bind(basketList[position], clickListener)
Log.d(TAG, "onBindViewHolder: basket list position is ${basketList[position]}")
}
Ask me if you still have any question
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.
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
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.