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
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.
I am working on a project which implements 2 views for each screen, a normal user view, and an admin view. The admin view is presented with a little more privileges than a normal user like deleting certain posts or the users themselves from the database.
Therefore, I set the visibility of those functional buttons to be GONE if the admin privilege is true (which I pass as a parameter value when initializing the adapter). But what I am struggling with, is where do I set the visibility, in the onCreateViewHolder method or onBindViewHolder method? I have right now set it in the onCreateViewHolder method because I had read on some Stackoverflow answer only that we should avoid heavy operations in onBindViewHolder method. But I would like to know a definitive answer.
Here are the code samples for reference:
The adapter class declaration:
class NoticesAdapter(options: FirestoreRecyclerOptions<NoticeModel>,
private val isAdmin: Boolean,
private val listener: INoticeListAdapter):
FirestoreRecyclerAdapter<NoticeModel, NoticesAdapter.NoticeViewHolder>(options)
onCreateViewHolder meothod:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoticeViewHolder {
val noticeListView = LayoutInflater.from(parent.context).inflate(R.layout.item_notice, parent, false)
val noticeListViewHolder = NoticeViewHolder(noticeListView)
if (!isAdmin)
{
noticeListViewHolder.deleteNoticeBtn.visibility = GONE
}
// On clicking the delete button on a notice by the admin
noticeListViewHolder.deleteNoticeBtn.setOnClickListener {
val noticeSnapshot = snapshots.getSnapshot(noticeListViewHolder.adapterPosition)
listener.deleteNoticeBtnListener(noticeSnapshot)
}
return noticeListViewHolder
}
The onBindViewHolder method:
override fun onBindViewHolder(holder: NoticeViewHolder, position: Int, model: NoticeModel) {
holder.noticeText.text = model.noticeText
holder.noticeAuthor.text = MyUtils.getUserName()
holder.noticePostDate.text = model.datePosted
holder.noticePostTime.text = model.timePosted
}
A RecyclerView.Adapter what it does is to: recycle items (as the name implies). The list doesn't have one view per item on the data source at the same time. The adapter makes sure to have enough views in memory in order to always render the list smoothly. When a row is leaving the field of view by scrolling, then that view is recycled to be re-used in the next entering view to the screen size.
This means that onCreateViewHolder is called only when a view is needed to be created. Generally at the start of the adapter, also when the user is scrolling fast or erratically and when the data set changes and is needed.
The other method onBindViewHolder is called every time the data on the row needs to be updated in order for the view to get updated. This is called every time a row is entering the view field of the screen.
So the textbook answer is: do it on onBindViewHodlder, because if the attribute isAdmin changes then that row will need to be updated. By doing it on onCreateViewHolder that would only happen one time when the row is created.
But, your isAdmin is a val on the constructor that can not be reassigned, so this means that when the rows are created the button will be hidden or visible forever. And this doesn't matter because your structure is to determine if is admin from another source that is separated from which the row data structure is derived from.
If in some case you want to:
make it more flexible and easier to maintain in the future
or maybe you know there is going to be a case where there is gonna be a list with admins and not admins rows
Then the solution is to move the isAdming attribute to your NoticeModel, that would imply changing your data structure.
If you want to verify anything sai above, get a data source with plenty of items and then add 2 logs, one on onCreateViewHolder and one in onBindViewHolder. You will see how on create is called only sometimes but on bind is called always.
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
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.
I'm trying to create a custom View that contains a list of CheckBoxes based on my database. This means I have to create the Views at run-time, and can't do it in XML. However, the method I'm using for this is very slow. Is there a faster method to create large amount of Views in code?
For example, with the 18 types in my database, it can take over 1 second to create all the CheckBoxes.
class FilterView : LinearLayout {
private fun init(types : List<Type>){
... setup
// Creating the CheckBoxes, this takes all the time.
checkboxes = Array(types.size, {
AppCompatCheckBox(context).apply {
text = types[it].type
CompoundButtonCompat.setButtonTintList(this, ColorStateList(states, intArrayOf(colours[it], colours[it])))
}
})
... add to view
}
What your looking for is a Recyclerview. It can all be explained here. The downvote was likely because this is assumed to be common knowledge or easily googled on your own. I was new once too. Here you go.