If aViewHolder's itemViewhassetActivated(.)called, that information is carried on after the view is recycled (i.e. the next will also be activated if the previous was).
Where would be a good place to save and store this information per item in the adapter rather than per itemView in the recycler; make items in the adapter a separate holder with an item and a boolean and then save it inonViewRecycled(.)as well as conditionally callsetActivated(.)again inonBind(.)?
Or are there better approaches altogether? (I was thinkingListViewmight be more intuitively, but unlessRecyclerViewis strongly discouraged for this typ of task, I'd prefer to use it.)
Well, I ended up creating a small local and private class that holds the status as well as the object. For now the status only includes a boolean isSelected, but I suppose it could be expanded and turned into a full fledged class in its own, when needed.
Related
I am doing a side project of making an app (with Java since I already know it). I have a recyclerview which loads some data via the room database library. The elements of the recyclerview are clickable.
My problem is I want the user to be able to sort the recyclerview so that the most recently accessed items go to the top.
My original idea was to assign the entities to have two variables - a String list_name which also serves as the id, and an Int order_of_access. Also, in my ViewModel I have a getAllLists method which returns a livedata list. I have an onChanged listener in the fragment activity which nicely updates the recyclerview when data is added/removed.
When the user adds a new list, it is assigned an order_of_access of the listsize (+1). But when the user deletes a group of lists, or clicks on a list, I want to update the order_of_access, say with an updateOrderAccess method.
Do you think this is the best way of doing what I want?
Where should I place updateOrderAccess and how would you recommend it be written? Since the method getAllLists returns livedata, it is tempting to put updateOrderAccess in an observer in the fragment (in onChanged) - but this will obviously create an infinite loop. It seems more in the correct philosophy to put it in the ViewModel, but then how would you suggest the updateOrderAccess method to be written? I'm having some trouble conceptualising what I need.
I hope the question is not too vague - I will update it if you need more details.
Where should I place updateOrderAccess and how would you recommend it
be written?
I am so sure that you must write it in the view model, as long as updateOrderAccess() is editing the list which is observable then you have andexpose by that the ui state then you have to put it in view model, and the observers will be notified ( in this case it is recycle view) and it will redraw the list in the order you offered.
note: do not you ever update the state(ui data) outside the state holder so you implement UDF (unidirectional Data Flow) pattern.
see the references below to read more about UDF so you never get confused where to declare your functions by letting the architicture lead you:
Guide to app architecture
ui layer
state holders and ui state
Do you think this is the best way of doing what I want?
i am not very sure that i got exactly what your app do, but it seems like you want to re-order the elements of recycle view depending on the ui event (click) or data change (deleting or adding new element), now you have two choices:
if the order is very importnat to you that much you want to keep it even if the app has been destroyed
then you have to add a field in the room entity represent the ordering (let us call it order) and whenever the user click on the recycle view you have to update the rooms field "order" which is "flow" or "liveData" or any observable type, that will tell the view model that there is a changing in the data, now the view model have to re-order the new data by the field "order" and pass it to the recycle view to show it.
if your app do not have to save the order changes after the app been destroyed
then you can simply do that:
create list which is called "orderedList" you will put the list items in it by the right order, and another list called "unorderlist" which have getAllLists
for the first case where the ordering is being changed by user click, you
can declare a function in viewModel then use it in the ui
controller (your activity or fragment), so whenever the list item is
clicked this function just re-order the orderedList elements ( which
is observable, so the changes reflect on the ui ) just by change the
clicked item position to the front of the list.
for the second case where the ordering changes by data changes like
add or delet a list item in the database, then you have to compare
the legnth of orderlist and unorderlist legnth, if unorderList is
longer then it is an add situation else it is a delete situation, in
adding case just add the last item of unorderList to the orderList,
else you have to check the deleted item and delete it from
orderList.
Context
So, I don't know if any of you has ever gone through the same situation but I've recently taken over an app and we have this RecyclerView on the main screen - because of an NDA I'll change a few things - that shows a list of apartments that you can rent - picture the AirBnB app - and if you tap on one of these apartment items you go to the apartment detail, where you have a bit more of functionality and features.
The thing is that we have way too many moving parts on the apartment list. For example, on each apartment ViewHolder you can:
Use a checkmark to specify if you are going to bring any pets with you.
A few UI items to specify how long are you going to stay.
An EditText to set how may people are going to come.
A Rent button that turns itself into a spinner and sends an API call.
A More Options button that expands the ViewHolder, showing a LinearLayout with yet more UI.
Picture something like this
This is actually a simpler example of what I really have. Let me tell you that it looks as if each ViewHolder could be a Fragment because of all the functionality that we have on each.
Now what's the problem here?
Recycling issues. If you scroll off, and scroll back to the same position you are supposed to keep the same state that you had on that ViewHolder, right? If you had checked a CheckButton that's supposed to be check. If you had written something on an EditText, that's supposed to be there. If you had expanded the More Options section, that's supposed to be expanded. You see where I'm going at?
What am I asking here?
Well, about feedback for a possible solution or improvement. I know what most of you would tell me here - because it is the same thing I thought at first - just move all that functionality into the apartment detail, keep that list as simple as possible. But it is not as simple, we have a large user base who is already used to this UI. Changing things so abruptly is not an option.
What do I have right now?
In my RecyclerView adapter I keep a collection of "State" objects which I use to save/restore the ViewHolder states, but it is getting way too big and way too complex. This may sound crazy, but it is there such thing as having a RecyclerList of Fragments? I just don't want to worry/bother about keeping the states of these ViewHolder anymore.
Notes
Sorry I haven't provided any code, but there's not much to show actually, as you may imagine the onBindViewHolder is just a humongous piece of code that sets the views with the data I fetch from the API plus the data that I store in these "State" objects. I save these "State" objects via the onViewDetachedFromWindows() hook from the adapter class that gets triggered when a ViewHolder scrolls off from screen. I wipe out these "State" objects when I fetch a new API response.
Any feedback is appreciated,
Thanks!🙇
Your post is vague in it's high-level description but I'll try to comment in a similar manner that may guide you towards solutions.
First, as was already mentioned Epoxy is a thing. As is adapter delegates. You may find those useful. However, you don't need a library to solve you problem - you need separation of concerns and architecture.
The thing is that we have way too many moving parts on the apartment list.
OK, so first suggestion is to stop having too many moving parts in the list. Each thing you listed could / should be it's own (custom) view that is driven by it's own ViewModel. A recycler view / view holder / adapter should be as stupid as possible. All those things should be doing is filling in boilerplate that Android requires. Actual logic should exist elsewhere.
If you scroll off, and scroll back to the same position you are supposed to keep the same state that you had on that ViewHolder, right?
No. Your ViewHolder should not maintain state. A ViewHolder holds views so Android doesn't have to re-inflate stuff over and over. It should not keep track of its state - it should be told what its current state is.
You should have a list of data objects (view models) that represent the current state of each item in the list. When you scroll off and back to the same position, you are supposed to re-bind the item that should be at that position to the view that represents it. Saving and clearing "state" objects should not be necessary - you should always have the current state on hand because it's the underlying data model driving your whole UI.
In my RecyclerView adapter I keep a collection of "State" objects which I use to save/restore the ViewHolder states, but it is getting way too big and way too complex
If something is too big and complex, break it down. Instead of having one giant-ass state object for each item, use composition. Make this item state have properties that represent pieces of the UI - PetModel, DateRangeModel, etc.
This may sound crazy, but it is there such thing as having a RecyclerList of Fragments? I just don't want to worry/bother about keeping the states of these ViewHolder anymore.
That does sound crazy because not only would this not solve your problem, you would probably actually make it significantly worse. You don't want to manage the state of a bunch of ViewHolders but you want to manage the states of a bunch of Fragments!? Bruh.
as you may imagine the onBindViewHolder is just a humongous piece of code that sets the views with the data I fetch from the API plus the data that I store in these "State" objects.
Again, break that up. You should not be slapping "data I fetched from the API" directly onto views. Invariably you will need to massage and transform raw data from an API before you display it. This should be handled by a dedicated object (again, ViewModel or some other structure). Again, views should be dumb. Tell them their state and that's it - don't do logic at this level.
Please read the Android Architecture Guide.
Also Google around for "Clean Architecture" - that seems to be all the range in Android these days.
And finally - here's some very rough pseudocode of how you could structure this to be more testable and maintainable.
From the bottom up:
ApiClient - responsible for just fetching the raw data from the API
endpoint or reporting an error.
ApiResponseModel - language-specific object representation
of the data you'll get from the API. Has info on the pet, dates,
guest count, etc. May contain submodels.
ItemDomainModel - client side representation of your data after transforming the data you'll get from the API.
Repository - uses the ApiClient to fetch the data as ApiResponseModel and transforms it into a ItemDomainModel object that makes more sense for your app.
ItemViewModel - Represents the UI state of a single item in the RecyclerView. Takes a ItemDomainModel instance and exposes the state of the UI based on the state of that model. This can be broken down if it's too complex (PetStateViewModel, DateRangeViewModel, GuestCountViewModel, etc)
ListViewModel - The top-level Android ViewModel that represents the state of the screen. Uses the Repository to fetch the data then constructs a list of ItemViewModels to feed into the RecyclerViewAdapter.
If you get those pieces in place, your view binding in the adapter should be stupid dumb:
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
// The adapter list should be a list of view models populated by the
// fragment after the ListViewModel returns a list of them from the fetch
val itemViewModel = itemViewModels[position]
// Populating this item view should just be a one-to-one mapping of the view model
// state - NO LOGIC. Dumb. Stupid. Tonto.
viewHolder.bringingPets.isChecked = itemViewModel.isBringingPets
viewHolder.guestCount.text = itemViewModel.guestCount
// ... etc, etc (if you use databinding this is a one-liner and even stupider)
// Set up your event listeners so interacting with this specific item in the list
// updates the state of the underlying data model
viewHolder.bringingPets.setOnCheckChanged { itemViewModel.isBringingPets = it.isChecked }
viewHolder.rentButton.onClickListener { itemViewModel.rentThis() }
// ... etc, etc
}
The goal is to do as little as possible here. Just update the state and wire up your callbacks that just delegate back to the ViewModel. Then, those UI states are driven by the logic in the view model. This is where you do business logic that determines how the UI should look.
class ItemViewModel(private val dataModel: ItemDomainModel) {
var isBringingPets: Boolean
get() = /* some business logic that determines if the checkbox is checked */
set(value) /* update underlying state and notify of changes */
// ... etc, etc, for guest count and other properties
fun rentThis() {
// Fire an event or update live data or invoke a callback that
// the fragment can use to respond
}
// ... etc, etc, for other functions that respond to UI events
}
In Summary
Refactor your code to break down the huge and complex logic into dedicated components that each have a simpler, specific focus, then compose them together to get the behavior you want. Good luck.
I use shared RecycledViewPool between some Fragments. I decided to try out MergeAdapter in one fragment. For that screen, I created a separate adapter for each viewType and overrode getItemViewType method to return layout ID as view type.
When I go to any other screen that has shared RecycledViewPool but is not using MergeAdapter I see some of the viewHolders from previous screen showing up. When I return back app crashes and in logs I see ClassCastException saying that ViewHolder1 cannot be casted to ViewHolder2.
My code looks like this:
recyclerView.setRecycledViewPool(sharedViewPool)
val adapter = MergeAdapter(adapter1, adapter2, adapter3, adapter4)
recyclerView.adapter = adapter
How to keep shared RecycledViewPool, but eliminate ClassCastException and stop showing ViewHolders in other screens where they should not be shown?
After digging more into the documentation I found that the problem was MergeAdapter with default configuration.
from documentation:
By default, MergeAdapter isolates view types of nested adapters from each other such that it will change the view type before reporting it back to the RecyclerView to avoid any conflicts between the view types of added adapters. This also means each added adapter will have its own isolated pool of RecyclerView.ViewHolders, with no re-use in between added adapters.
If your RecyclerView.Adapters share the same view types, and can support sharing RecyclerView.ViewHolder s between added adapters, provide an instance of MergeAdapter.Config where you set MergeAdapter.Config.isolateViewTypes to false. A common usage pattern for this is to return the R.layout. from the RecyclerView.Adapter.getItemViewType(int) method.
I also read this in article written by Florina Muntenescu and for some reason I thought that isolateViewTypes is only affecting adapters added to one MergeAdapter. But since in default config it will modify returned view types for each ViewHolder it might (most likely will) cause inconsistency in shared RecycledViewPool. Not sure if it is a bug or expected behaviour. Would be great to hear something from team working on RecyclerView.
Solution was setting isolateViewTypes to false.
val adapter = MergeAdapter(
MergeAdapter.Config.Builder().setIsolateViewTypes(false).build(),
adapter1,
adapter2,
adapter3,
adapter4
)
I think it is pretty easy mistake to make and I hope this solution will save some time for others.
I can see setting adapter.
setHasStableIds(true);
will improved RecyclerView performance, since it is improving lots of performance, why it is not default enabled in RecyclerView, Is there any limitations while setting setHasStableIds(true)?
The reason why it is not the default is because the data backing the adapter might not have stable Id's and the RecyclerView does not know this.
Yes there is a limitation one using setHasStableIds(true) - your data has to have stable Id's
You either need override getItemId(int position) to return a unique Id that would represent each Data Item, simply return the HashCode if you cannot think of anything better.
Or
If using something like a CursorAdapter this automatic has stable Id's as it uses the database table _id column.
The reason is when we are going to just displaying the data in recycler-view that case no need to unique id of each item and no animation required. so it's default its false.
But when we are going to perform some action in the dataset and update the recycler-view that case its need unique id for particular item and refreshing animation (blinking) it need.so we set as true
private fun turnOnAllItems() {
items.forEachIndexed { index, item ->
val viewHolder = recyclerView.findViewHolderForAdapterPosition(index)
as SwitchableItemViewHolder
viewHolder.switchButton.isChecked = false
}
}
What this does, is it also changes list items object values isEnabled to false. Looks weird to me, as I actually change viewHolder attribute. Why is this happening? How to avoid this?
I strongly believe that you are doing it the wrong way. RecyclerView is meant to display already modified data, meaning that you have a set of it.
Let's say, 10 tables in restaurant, and at some point table #4 becomes available for new customer and you want to indicate that.
A good approach would be to modify your list of tables somewhere outside RCV, even fragment or activity will do, and then just graphically update (all or just one) item by means of RCV.
Here's a little article I made to illustrate how to properly use RecyclerView, hope it will help you