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
Related
I'm using 3 different view types in a recyclerview. since one of them contains a list of views, it is kind of heavy and needs more time to be created. so whenever the recyclerview arrives at the beginning of these views and needs to create them (in addition to binding), I experience jank.
in order to solve the issue, I need the recyclerview to create as many views as it needs at the initialization level and then when the user scrolls just binds the data to already-created views. how can I force recyclerview to create all needed views in begining?
I already tried to create a RecycledViewPool which contains the required view holders, but the pool does not accept the added viewHolders for some reason:
RecyclerView.RecycledViewPool recycledViewPool = recyclerView.getRecycledViewPool();
recycledViewPool.putRecycledView(recyclerViewAdapter.onCreateViewHolder(recyclerView, Model.TYPE_1));
collectionsRecyclerView.setRecycledViewPool(recycledViewPool);
You should use recyclerViewAdapter.createViewHolder instead of recyclerViewAdapter.onCreateViewHolder because (documentation):
This method calls onCreateViewHolder(ViewGroup, int) to create a new RecyclerView.ViewHolder and initializes some private fields to be used by RecyclerView.
Also make sure to call recyclerViewAdapter.setMaxRecycledViews(Model.TYPE_1, MAX) with a sufficiently large MAX value.
This works great for me even though this interesting article in general warns against using putRecycledView() manually:
Using putRecycledView() manually, e.g. in order to prepare some ViewHolders beforehand, is a bad idea, though. You should create ViewHolder only in onCreateViewHolder() method of your Adapter, otherwise the ViewHolders can appear in states that RecyclerView doesn’t expect.
E.g. the correct view type is set in createViewHolder() method right after the Adapter call, and the field is package local, so you can’t set it yourself.
However I see no problem since we are calling createViewHolder().
Alternatively you could simply use your own pool:
val pool = LinkedList<H>()
var ini = true
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
if (ini)
for (i in 0 until 10)
pool.push(createYourViewHolder())
ini = false
// if it needs more than expected
if (pool.size == 0) pool.push(createYourViewHolder())
return pool.pop()
}
That way, when the recyclerview is created, becomes visible and requests the first view (and no scrolling should be happening), reserve views are created, and following calls to onCreateViewHolder will take virtually no time, see here.
This is what I used previously and also works.
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've created an app that has a list of cards within a RecyclerView that each have functionality of their own. I wanted to have each card choose the next color from an array defined in my colors.xml. In order to accomplish this, within my ViewHolder initialization, I set the background color of the card using cardContainer.setBackgroundColor(colors[this.layoutPosition % colors.size]. This would make it so that the colors would be cycled when more cards are created. However, I seem to be encountering the issue where my layout position is negative despite there being a set number of cards (25) created at the beginning.
While trying to search around and find the cause, I read here that if you call notifyDataSetChanged() the adapterPosition will become -1. While I am not using adapterPosition here, I thought that maybe it would be a similar issue, however, I am not adding any additional data at the time of the creation of the list items.
My ViewHolder code can be seen below. This is where the issue arises, but if any additional code is necessary feel free to ask.
class ViewHolder(itemView : View, private val listener: HabitClickListener) : RecyclerView.ViewHolder(itemView) {
val habitTitle: TextView = itemView.habitTitle
val streak: TextView = itemView.dayCounter
val cardContainer: LinearLayout = itemView.cardContainer
private val decreaseCounterButton : Button = itemView.decreaseCounterButton
private val increaseCounterButton : Button = itemView.increaseCounterButton
init {
chooseCardColor() // Choose the color for each card from the available colors
itemView.setOnClickListener {
listener.onCardClick(this.layoutPosition)
}
decreaseCounterButton.setOnClickListener {
listener.onDecrease(this.layoutPosition)
}
increaseCounterButton.setOnClickListener {
listener.onIncrease(this.layoutPosition)
}
}
private fun chooseCardColor() {
val colors = itemView.resources.getIntArray(R.array.cardColors)
cardContainer.setBackgroundColor(colors[this.layoutPosition % colors.size])
}
}
I will try to simplify this further, you should use the getAdapterPosition of ViewHolder
In recyclerview, storing the data and displaying the data are two separate things(Notice how you can use different managers(LinearLayoutManager, GridLayoutManager) to present the data in a different way.When some data changes in recyclerview, it notifies the ui to change what is shown in the screen. Even though it is really small, there is a delay between the change in the content of recyclerview and change in layout, that's why these two behave differently.
My information in this may be outdated but also don't just use the position variable as it can be inconsistent when another element is added/deleted to recyclerview due to how onBindViewHolder()(existing variables position wasn't updated when a new element is added/deleted) behaves. Instead use getAdapterPosition().
Edit: Quick fix if you don't want to deal with viewHolder gimmicks.
Add a new field to your custom object which decides what color it should be. Then make this calculation in your fragment/activity by looking at the index of your object in the list instead of doing the calculation in the viewHolder. Now you can set the color you want inside the viewHolderby looking at your object's new field. Of course you should be careful when adding/deleting a new object when you do this, but same holds true when you do it via viewHolder
My outer RecyclerView crashes either with
IllegalArgumentException: Scrapped or attached views may not be recycled. isScrap:false isAttached:true...
or
IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
Like the title suggests I have an RecyclerView in the list item layout of the first RecyclerView. This layout is used to display messages and the
inner RecyclerView to display attachments that come with the message. The inner RecyclerViews visibility is set to either GONE or VISIBLE depending whether the message has any attachments or not. The simplified outer list item layout looks like this
ConstraintLayout
TextView
TextView
TextView
RecyclerView
And the part of the adapter that handles the inner RecyclerView looks like this
private fun bindFiles(message: Message?) = with(itemView) {
if (message != null && message.attachments.isNotEmpty())
{
sent_message_attachments.setAsVisible()
sent_message_attachments.layoutManager = GridLayoutManager(this.context,Math.min(message.attachments.size,3))
sent_message_attachments.adapter = AttachmentAdapter(message.attachments)
sent_message_attachments.itemAnimator = null
sent_message_attachments.setHasFixedSize(true)
}
else{
sent_message_attachments.setAsGone()
sent_message_attachments.adapter = null
sent_message_attachments.layoutManager = null
}
}
The bug has something to do with the way I fetch the attachments in the inner adapter since once I disable the part that start the download process, everything is fine. There's no problem when loading images from the device, but once I start the download process, everything goes to hell. This is the part that handles images and kicks off the download process in the inner adapter. I have functions for videos and for other file types that are pretty much the same exact thing but use slightly different layout.
private fun bindImage(item: HFile?) = with(itemView) {
if (item != null)
{
if (item.isOnDevice && !item.path.isNullOrEmpty())
{
if (item.isGif)
{
attachment_image.displayGif(File(item.path))
}
else
{
attachment_image.displayImage(File(item.path))
}
}
else
{
//TODO: Add option to load images manually
FileHandler(item.id).downloadFileAsObservable(false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
//TODO: Show download process
},
{ error ->
error.printStackTrace()
//TODO: Enable manual retry
},
{ notifyItemChanged(adapterPosition)} //onComplete
)
}
}
}
I use the same structure as above in my DiscussionListAdapter to load discussion portraits (profile pictures etc.) and it does not have the same issue.
These are the extensions functions used to inflate the viewHolders and to display the images
fun ViewGroup.inflate(layoutRes: Int): View
{
return LayoutInflater.from(context).inflate(layoutRes, this, false)
}
fun ImageView.displayGif(file:File){
GlideApp.with(context).asGif().load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
fun ImageView.displayImage(file:File){
GlideApp.with(context).load(file).transforms(CenterCrop(), RoundedCorners(30)).into(this)
}
I've been on this for the past couple of days and just can't get my head around it. Any help in any direction is greatly appreciated. I know my explanations can be a bit all over the place so just ask for clarification when needed :)
UPDATE
I have now been able to produce this with a GridLayout as well as with RecyclerView. It's safe to assume that the nested RecyclerViews were not the culprit here. I even tried to ditch the Rx-piece that handled loading the images and created an IntentService for the process, but the same crashes still occur.
With GridLayout I mean that instead of having another adapter to populate the nested RecyclerView I use only one adapter to populate the message and to inflate and populate views for the attachments as well and to attach those views to the nested GridLayout.
The crash happens when I start to download a file and then scroll the view, that is supposed to show the downloaded file, out of the screen. That view should get recycled but for some reason the download process (which in my test cases only takes around 100ms-400ms) causes the app to throw one of the two errors mentioned in the original question. It might be worth noting that I'm using Realm and the adapter takes in a RealmResults<Message> list as it's dataset. My presenter looks for changes in the list and then notifies the adapter when needed (changed due to the implementation of IntentService).
This is how I'm capable to reproduce this time and time again:
Open a discussion that has messages with attachments
Start to scroll upwards for more messages
Pass a message with an attachment and scroll it off screen while it's still loading
Crash
There is no crash if I stop and wait for the download to complete and everything works as intended. The image/video/file gets updated with a proper thumbnail and the app wont crash if I scroll that out of view.
UPDATE 2
I tried swapping the nested ViewGroup for a single ImageView just to see is the problem within the nestedness. Lo and behold! It still crashes. Now I'm really confused, since the DiscussionListAdapter I mentioned before has the same exact thing in it and that one works like a charm... My search continues. I hope someone, some day will benefit from my agony.
UPDATE 3
I started to log the parent of every ViewHolder in the onBindViewHolder() function. Like expected I got nulls after nulls after nulls, before the app crashed and spew this out.
04-26 21:54:50.718 27075-27075/com.hailer.hailer.dev D/MsgAdapter: Parent of ViewHolder: android.view.ViewOverlay$OverlayViewGroup{82a9fbc V.E...... .......D 0,0-1440,2168}
There's a method to my madness after all! But this just poses more questions. Why is ViewOverlay used here? As a part of RecyclerView or as a part of the dark magicians plans to deprive me of my sanity?
Sidenote
I went digging into RecyclerViews code to check if I could find a reason for the ViewOverlaymystery. I found out that RecyclerView calls the adapters onCreateViewHolder() function only twice. Both times providing itself as the parent argument for the function. So no luck there... What the hell can cause the item view to have the ViewOverlay as it's parent? The parent is an immutable value, so the only way for the ViewOverlay to be set as the parent, is for something to construct a new ViewHolder and supply the ViewOverlay as the parent object.
UPDATE 4
Sometimes I amaze myself with my own stupidity. The ViewOverlay is used because the items are being animated. I didn't even consider this to be an option since I've set the itemAnimator for the RecyclerView as null, but for some odd reason that does not work. The items are still being animated and that is causing this whole charade. So what could be the cause of this? (How I chose to ignore the moving items, I do not know, but the animations became very clear when I forced the app to download same picture over and over again and the whole list went haywire.)
My DiscussionInstanceFragment contains the RecyclerView in question and a nested ConstraintLayout that in turn contains an EditText for user input and a send button.
val v = inflater.inflate(R.layout.fragment_discussion_instance, container, false)
val lm = LinearLayoutManager(context)
lm.reverseLayout = true
v.disc_instance_messages_list.layoutManager = lm
v.disc_instance_messages_list.itemAnimator = null
v.disc_instance_messages_list.adapter = mPresenter.messageAdapter
This is the piece that handles the initialization of the RecyclerView. I'm most definitely setting the itemAnimator as null, but the animations just wont stop! I've tried setting the animateLayoutChanges xml attribute on the root ConstraintLayout and on the RecyclerView but neither of them worked. It's worth mentioning that I also checked whether the RecyclerView had an itemAnimator in different states of the program, and every time I check the animator, it is null. So what is animating my RecyclerView?!
I have faced the same issue
Try this in your child RecyclerView it works for me
RecyclerView childRC = itemView.findViewById(R.id.cmol_childRC);
layoutManager = new LinearLayoutManager(context);
childRC.setItemAnimator(null);
childRC.setLayoutManager(layoutManager);
childRC.setNestedScrollingEnabled(false);
childRC.setHasFixedSize(true);
now set your Adapter like this
ArrayList<Model> childArryList = new ArrayList<>();
childArryList.addAll(arrayList.get(position).getArrayList());
ChildOrderAdapter adapter = new ChildOrderAdapter(context, childArryList);
holder.childRC.swapAdapter(adapter, true);
hope this helps
I finally figured out what was causing this. In my DiscussionInstanceView I have a small view that is animated into and out of view with ConstraintLayout keyframe animations. This view only shows the download progress of the chat history and is used only once, when the discussion is first opened. BUT since I had a call to hiding that view every time my dataset got updated, I was forcing the ConstraintLayout to fire of an animation sequence thus making everything animate during the dataset update. I just added a simple check whether I was downloading the history or not and this problem got fixed.
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.