Why are highlighted items lost after scrolling in recylerview android - android

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.

Related

is there any alternate for arraylist.remove(element) function?

my recycler view reloads with spinner value, selected data is stored in a global arraylist, and here is my recycler view code to do the selection and deselection. Selection and deselection works just fine, but when i select and change spinner value, and then come to back original spinner value, where items are already selected, deselction happens, but item is not removed from the global arraylist. While debugging i found that cursor reaches there, but i dont know why .remove() isnt working. Is there any alternative for it or am i doing wrong? Is there anything i should know that why isnt the item removed.
.
.
.
class RecyclerViewAdapter(val dataList:ArrayList<ModelClass>,val onItemClicked: (Int) -> Unit):RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
object ob {
val dataSelected = ArrayList<ModelClass>()
val hm = HashMap<ModelClass,String>()
}
fun setData(listModel: List<ModelClass>) {
dataList.clear()
dataList.addAll(listModel)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = ItemViewBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return ViewHolder(binding, parent.context)
}
#SuppressLint("ResourceAsColor")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bindItems(dataList[position])
}
override fun getItemCount() = dataList.size
inner class ViewHolder(
val binding: ItemViewBinding,
val context: Context
) : RecyclerView.ViewHolder(binding.root) {
var count=0
#SuppressLint("ResourceAsColor")
fun restore() {
for (i in 0 until ob.dataSelected.size) {
for (j in 0 until dataList.size) {
if (ob.dataSelected[i].sku_code == (dataList[j]).sku_code) {
if (adapterPosition == j) {
itemView.isSelected = true
itemView.setBackgroundColor(R.color.black)
count=count+1
println("****")
}
}
}
}
if(!itemView.isSelected){
itemView.isSelected = false
itemView.setBackgroundResource(0)
}
}
#SuppressLint("ResourceAsColor", "ResourceType")
fun bindItems(data: ModelClass) = with(binding) {
binding.itemQuant.text = data.item_quant
binding.itemName.text = data.item_name
binding.mfgName.text = data.mfg
binding.quantity.text = data.item_stock.toString()
count=0
restore()
itemView.setOnClickListener {
count += 1
var isPresent:Int
if (count%2 == 0){
isPresent=1
}
else
{
isPresent=0
}
if (isPresent == 1) {
it.setBackgroundResource(0) //works
ob.dataSelected.remove(dataList[adapterPosition]) //doesnt work if spinner value is changed and changed back. works while still on same screen.
} else {
if (isPresent == 0) {
it.setBackgroundColor(R.color.black)
ob.dataSelected.add(dataList[adapterPosition])
}
// onItemClicked.invoke(adapterPosition)
}
}
}
}
}
Are you sure it's not being removed? Have you set a breakpoint and checked the contents of the array (or just logged it)? Because it looks like when you're binding a viewholder, you just check if the item is in dataSelected, and if it is you set the view's selected and backgroundColor values. It doesn't look like you change them back if it's not in dataSelected, so that selected appearance "sticks".
It looks like you're trying to reset them with this:
if(!itemView.isSelected){
itemView.isSelected = false
itemView.setBackgroundResource(0)
}
but that only works if itemView.selected hasn't been set to true, which it has if your item is in dataSelected. You need to do it like this:
for (i in 0 until ob.dataSelected.size) {
for (j in 0 until dataList.size) {
// rolling multiple conditions into a single value makes it easier to do an if/else
val selected = ob.dataSelected[i].sku_code == (dataList[j]).sku_code && adapterPosition == j
if (selected) {
itemView.isSelected = true
itemView.setBackgroundColor(R.color.black)
count=count+1
println("****")
} else {
itemView.isSelected = false
itemView.setBackgroundResource(0)
}
}
}
That way you're always updating the contents of the view holder to reflect the state of the current item. That's especially important in RecyclerViews, because the point of those is that the ViewHolders get reused to display different items, and any View attributes you don't set when binding (e.g. colours, checkbox statuses) will just stay on whatever they were last set to when displaying some other item.
btw, you can simplify that looping situation by just checking if anything in dataSelected has the SKU you're looking for:
val itemSku = datalist[adapterPosition].sku_code
val selected = ob.dataSelected.any { it.sku_code == itemSku }
ideally you wouldn't be using adapterPosition to find the current item either - the item you're matching is passed in to bindItems (as data), you could just pass that to your restore function
edit if the items aren't being removed from dataSelected, at a guess it's because the object you're fetching from dataList "doesn't exist" in dataSelected. By "doesn't exist" I mean there isn't an object that equals the one you're trying to remove.
ob.dataSelected.remove(dataList[adapterPosition])
I'm assuming that might be the case because you're not doing straight object comparisons when you're checking if an item is in the selected array, you're specifically comparing their sku_code values instead
if (ob.dataSelected[i].sku_code == (dataList[j]).sku_code)
If you can't just do if (obj.dataSelected[i] == dataList[j]), then remove won't work either - it's the same check. So maybe you need to look into making those objects equal (e.g. using data classes if that works for what you're doing), or use removeAll with a predicate (remove only works with a specific item):
ob.dataSelected.removeAll { it.sku_code == dataList[adapterPosition].sku_code }
You'll still have the problem of the UI not being updated though, like the first part of the answer explains! So you might have two problems here
change this line
ob.dataSelected.add(dataList[adapterPosition])
to this
ob.dataSelected.add(dataList[adapterPosition].copy)
Don't forget to call notfiyDatasetChanged() after that

Kotlin:change background color of RecyclerView item by click on it

I want to change background color of recycler view row(item) by clicking on it, i use kotlin.
i tried different methods,The item that is clicked changed background color correctly, but when I scroll down I see that another item has also changed background color.
In fact, for every click on the item, another item in the part of the list that is not in the user's view is also affected.
Please explain this problem with a complete example.
Please be sure to test it yourself
I really need to fix this
Thanks a lot
it's quite easy to do :) You need to just remember how the RecyclerView works:
It creates few ViewHolders
When you scroll your list, the ViewHolders are reused.
If you have for example 100 items on the list, it will create 5-6 ViewHolders and will reuse them.
So having that in mind, when onBindViewHolder() is called, the easiest thing you can do is to check some state of the item, and then choose background color of the item. In this scenario you can select/deselect multiple items in the list. If you want to have only one checked, you will need to change the items.map {} function a little bit:
Please keep in mind that I wrote below from my head, you will also need to override some more functions in adapter etc.
class MyAdapter(private val onItemClick: (YouItem) -> Unit): RecyclerView.Adapter() {
private var items: List<YourItem>
fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val payload = items[holder.adapterPosition]
holder.itemView.setOnClickListener {
onItemClick(payload)
}
holder.itemView.background = if(payload.someState) firstBackground else otherBackground
}
fun updateItems(updatedItems: List<YourItem>) {
items = updatedItems
notifyDataSetChanged()
}
}
class YourActivity: Activity() {
private lateinit var items: List<YourItem>
fun onCreate(savedInstanceState: Bundle) {
super.onCreate(savedInstanceState)
...
items: List<YourItem> = getItemsFromSomewhere()
val adapter = MyAdapter { clickedItem ->
val updatedItems = items.map {
if(it == clickedItem) {
it.copy(someState = !it.someState)
} else {
it
}
items = updatedItems
adapter.updateItems(updatedItems)
}
}
}
data class YourItem(val someState: Boolean)
This is mostly because of RecyclerView reuses the view(item row) so that the view need not to be generated multiple times. you can solve the problem by setting the background only for a particular item by changing the value of the the object and for all others keep it default value.
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
if(position == selected_item) {
holder.view.setBackgroundColor(Color.parseColor("#00aaff"));
} else {
holder.view.setBackgroundColor(Color.parseColor("#00000")); //actually you should set to the normal text color
}
}
or
public void onItemClick() {
Model model = itemList.get(clickedPosition);
model.setBgColor = Color.parseColor("#00aaff");
}
#Override
public void onBindViewHolder(MyViewHolder holder, int position) {
Model model = itemList.get(position);
holder.view.setBackgroundColor(model.getBgColor());
}

findviewwithtag gives null when recyclerview gets scrolled

I am having a custom gallery implemented with recyclerView, I want to deselect the selected items from other fragment.
To do that I have set a tag to each view as -
#Override
public void onBindViewHolder(GalleryAdapter.ViewHolder viewHolder, int i) {
String gitem = galleryList.get(i);
viewHolder.itemView.setTag(gitem); // Setting the tag
....
}
to deselect I am finding the view by tag as -
recyclerView_gallery.findViewWithTag(String.valueOf(showImageView.getTag())).performClick();
It is working when a gallery recyclerview scroll is on that Image but when I scroll down and go to that fragment - findViewWithTag is returning null, but when I scroll to that Image and again do same I found no error.
I have tried this -
recyclerView_gallery.getLayoutManager().scrollToPosition(position);
recyclerView_gallery.findViewWithTag(String.valueOf(showImageView.getTag())).performClick();
also, I have tried -
recyclerView_gallery.findViewHolderForAdapterPosition(position);
but the result is the same, it not working if recyclerView gets scrolled.
RecyclerViews recycle all views which are not currently visible to same memory.
This means if you scroll too far the view just doens't have the tag anymore so findViewWithTag will return null.
What i would do is setup a List somewhere which contains all items that are supposed to be selected and then check each time in the onBindViewHolder and perform an action accordingly. This method get's called each time an item is scrolled into/out of the screen.
The code could look something like this:
public class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
public List<String> selectedTags = new ArrayList<>();
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
Log.d("TAG", "onBindViewHolder was called for position: " + position);
TextView textView = ((ViewHolder) holder).textView;
textView.setTag(tags.get(position));
textView.setOnClickListener(...)
if (selectedTags.contains(textView.getTag())) {
Log.d("TAG", position + " do something with this item.");
textView.performClick();
}
}
...
}
public class MainActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
...
List<String> someTags = new ArrayList<>();
someTags.add("someTags");
((RecyclerViewAdapter) recyclerView.getAdapter()).tags = someTags;
...
}
}
Recycler view creates only 4 or 5 views more than the visible view count and rebinds/reuses the views that are not visible to user and hence it is called RecyclerView. OnCreateView method is called to create the required views and onBindViewHolder is called every time the view is bound with data.
public void onBindViewHolder(GalleryAdapter.ViewHolder viewHolder, int i) {
String gitem = galleryList.get(i);
viewHolder.itemView.setTag(gitem); // Setting the tag
....
}
When the view is bound with data "A" , its tag is "A" and when it is reused for item "B", its tag becomes "B" and so when you uses findViewByTag("A"), it returns null because the tag has already changed into "B". You can disable this feature by wrapping the recycler view inside a NestedRecyclerView since NestedRecyclerView creates all the views required and there will not be no view reusing. (It is very easy, but I would not recommend it).
The other approach is to save the selected items/itemId somewhere like this. Suppose your galleryList is a String list,
private Set<String> selectedItemList = new HashSet<>();
private List<String> galleryList = new ArrayList<>();
public void onBindViewHolder(#NonNull ViewHolder holder, int position) {
String gItem = galleryList.get(position);
holder.populateUi(gItem); //populate normal ui
if (selectedItemList.contains(gItem)) {
holder.markSelected(); //mark the ui as selected
} else {
holder.markUnSelected(); //mark the ui as unSelected
}
}
and I think you can get the position of the clicked item somehow, if so you may update the ui as follow.
public void notifySelected(int... positions) {
for (int position:positions) {
String gItem = galleryList.get(position);
selectedItemList.add(gItem);
notifyItemChanged(position);
}
}
public void notifyUnSelected(int... positions) {
for (int position:positions) {
String gItem = galleryList.get(position);
selectedItemList.remove(gItem);
notifyItemChanged(position);
}
}
If you can't get the position, but the item only, you may change like this
int index= galleryList.indexOf(item);
if(index>-1){
selectedItemList.add(galleryList.get(index));
}
The better approach is to use the itemChanged Method with payload as it is smoother.
Sorry for code style, as I hadn't coded in Java for a long time.
As #WHOA mentioned
When the view is bound with data "A", its tag is "A" and when it is reused for item "B", its tag becomes "B" and so when you use findViewByTag("A"), it returns null because the tag has already changed into "B".
And it is a possible reason why you can't find your view by tag. But I don't suggest you save any other list with ids or selected tags. Instead of this, you can add a boolean field to your data model which will tell your adapter is it selected or not or you can create a wrapper for your model with the same field (Idk what is better for your case). Then you will be sure about is item selected or not, also you will be able to store your data as selected/unselected in database or another storage.
Some code samples to be clear:
Adapter
class RvAdapter(private val items: List<ListItem>) : RecyclerView.Adapter<MyViewHolder>() {
fun selectItems(vararg ids: Int) {
items.filter { ids.contains(it.id) }
.forEach { it.isSelected = true }
notifyDataSetChanged()
}
fun unSelectItems(vararg ids: Int) {
items.filter { ids.contains(it.id) }
.forEach { it.isSelected = true }
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false))
}
override fun getItemCount(): Int = items.size
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val currentItem = items[position]
holder.bind(currentItem)
}
}
ViewHolder:
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
fun bind(item: ListItem) {
(itemView as TextView).text = item.id.toString()
itemView.isSelected = item.isSelected // you decide how to mark your view as selected/unselected
}
}
And ListItem (change id: Int on whatever you need):
data class ListItem(val id: Int, var isSelected: Boolean)
IMO, in this way, it will be more clear for you and other people to read this code or edit in the future.
Hope it will help!

notifyDataSetChanged in viewHolder class

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;
}
}

How can I shuffle the contents of a RealmResults object

I have a RealmResults object which holds output of a .where.findAll() query. I am showing contents of this object in a RecyclerView, but since the contents of the RealmResults object are static, it shows same set of records evertime the RecyclerView is opened. I want to shuffle (randomize) the contents of RealmResults so that I can show different values that are in the RealmResults in my RecyclerView. Please suggest some possible ways in which I can perform the same.
You should not randomize the list itself, you should randomize your access of it (the indices).
Build a list that contains the indexes from [0, n-1]
List<Integer> indices = new ArrayList<>(realmResults.size());
for(int i = 0; i < realmResults.size(); i++) {
indices.add(i);
}
Shuffle it
Collections.shuffle(indices);
Then when you pick the view holder data, index realm results with indexed random position
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
if(holder instanceof YourHolder) {
YourHolder yourHolder = (YourHolder) holder;
RealmData realmData = realmResults.get(indices.get(position));
//init data for your holder
}
}
For future references, when extending RealmRecyclerViewAdapter, if you enabled the autoUpdate feature, the above solution will not be enough in cases of updates.
Here is a solution based on the same idea, handling the updates.
Here I ignore the update type and just reset the whole shuffling state if the item count changes. It's not hard to adapt for more specific events (itemRangeAdded, itemRangeRemoved, ...) you just need to update indicesMapList and displayedMapList accordingly.
This adapter handles shuffling/sorting and is ready to handle filtering as well (it was not asked but pulled this from a code I did, you can remove unnecessary part if not wanted).
(To filter just update the displayedMapList with indices of items you want to display, sort/shuffle operations will preserve the filter)
abstract class BaseRealmAdapter<T : RealmObject, S : RecyclerView.ViewHolder>(data: OrderedRealmCollection<T>,
autoUpdate: Boolean) :
RealmRecyclerViewAdapter<T, S>(data, autoUpdate) {
private val indicesMapList = arrayListOf(*(0 until getRealItemCount()).toList().toTypedArray())
val displayedMapList = ArrayList(indicesMapList)
var shuffled = false
private set
/** Displayed item count (after possible filter) */
override fun getItemCount() = displayedMapList.size
override fun getItem(index: Int) = super.getItem(displayedMapList[index])
/** Unfiltered item count */
fun getRealItemCount() = super.getItemCount()
fun shuffle() {
if (getRealItemCount() == 0)
return
shuffled = true
Collections.shuffle(indicesMapList)
// Keep same order
displayedMapList.sortBy { indicesMapList.indexOf(it) }
notifyDataSetChanged()
}
fun sort() {
if (getRealItemCount() == 0)
return
shuffled = false
indicesMapList.sort()
displayedMapList.sort()
notifyDataSetChanged()
}
protected fun resetIndicesMap(notifyDataSetChanged: Boolean = true) {
shuffled = false
indicesMapList.clear()
indicesMapList.addAll(0 until getRealItemCount())
resetDisplayedList()
if (notifyDataSetChanged)
notifyDataSetChanged()
}
protected fun resetDisplayedList() {
displayedMapList.clear()
displayedMapList.addAll(indicesMapList)
}
protected fun getIndicesMapList() = ArrayList(indicesMapList)
override fun onAttachedToRecyclerView(recyclerView: RecyclerView?) {
registerAdapterDataObserver(DataObserver())
super.onAttachedToRecyclerView(recyclerView)
}
/** This implementation will cancel any current shuffled state when adding items */
inner class DataObserver : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
// Item(s) added or removed: this will cancel filtering
if (getRealItemCount() != indicesMapList.size)
resetIndicesMap(true)
}
}
}

Categories

Resources