I have a RecyclerView that contains views that can be quite complex and their inflation make the scrolling choppy if they are inflated just in time. My idea was that I could pre-inflate the views in a background thread. I've tried it and it works really nice on the emulator as well as on my Pixel 3. To be clear, they are only inflated and not added to the parent ahead of time.
I found this SO thread, where they say that there seems to be an issue with Samsung Galaxy phones, but the thread is almost 10 years old, so i am not sure if this is still an issue.
Google of course says not to do it. See here. Although the reasons they mention (mostly memory leaks) don't apply in my case. Or at least I don't see a possible Activity leak here.
This is simplified what I am doing:
class MyRecyclerAdapter(
private var items: List<Item>
) : RecyclerView.Adapter<MyViewHolder>() {
// Map to hold the pre-cached views
private val viewCache = mutableMapOf<Int, Deferred<View?>>()
companion object {
// Only inflate one view at a time
val serialCoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
}
init {
items.forEachIndexed { index, it ->
viewCache[index] = CoroutineScope(serialCoroutineDispatcher).async {
createViewForItem(it) // View is created asynchronously here
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
runBlocking {
MyViewHolder(
// If no view in cache, inflate here
viewCache.remove(viewType)?.await() ?: createViewForItem(items[viewType])
)
}
...
Has anyone tried something similar? Or can someone speak to why I should not do it (besides leaking the Activity which is not an issue here)?
FYI: I'm aware of AsyncLayoutInflater, but I cannot use it for other reasons.
Related
Hi and thanks for reading in advance,
I have built a small project testing an adapter to populate a main activity view with a custom xml template [this template holds some fields that are populated by some simple test data and images inside the project]
The build is fine afaik, I have debugged it as best I can to yield no errors and I cant actually find the error as i follow the build in debug so it's likely my noob-like experience with kotlin, android studio and maybe the later development approach changes.
My best guess after following it through a few (way more ha) times, at this stage it seemed to be going wrong during the android part of the build versus any prep and adapter code so it could be some settings I havent invoked maybe...
Also probably importantly: I saw it pull in the recyclerView during debug(prior to any customisation from the imported xml layout im using), the visual then disappears, but i do see all the code being populated by the functions ive created in MainActivity and the adapter does seem to return to MainActivity with the respective data constructed.
I enclose the project zipped as there is no discernable error code or post runtime exit code that i can find [thats not to say there isn't one, I'm newoob to the IDE :( ]
I'm targeting an Android 7.0 600*1024 mdpi using API 24 on a 7" (LH flip)portrait orientation x86 emulator
I'm using Android Studio 2021.1.1 patch 3 if that's important to know too
Thanks again for anyone who can take a peek at it to help me along, im proper stuck, been so for couple days now, thinking of a complete redo again :/
:)
I enclose a link to the zipped file below [due to no errors in debug to provide here]
[Edit: Cripes I forgot to mention I'm also using CircleImageViewer, not sure if that causing an issue but it doesn't appear to be ]
[Final edit: As can be seen below this was answered by Tyler V who understood my problem better than I could pose the question - please see below for a complete resolution/ refactor of code by them that works correctly - thanks again to Tyler V]
project on googledrive
I took a look at you app, the error it is throwing (look in the Logcat tab!) is this
Process: com.example.kayakthing, PID: 4289
java.lang.IndexOutOfBoundsException: Index: 5, Size: 5
at java.util.ArrayList.get(ArrayList.java:411)
at com.example.kayakthing.HiresAdapter.onBindViewHolder(HiresAdapter.kt:34)
at com.example.kayakthing.HiresAdapter.onBindViewHolder(HiresAdapter.kt:11)
Problem
The reason it is throwing this is because you are supplying different length arrays to your adapter. Take a look at the adapter code below - in your current code the lengths of nameList and clientDetailsList are both 7 but the length of hireImageList is 5. Since you define getItemCount based on the length of nameList - the adapter crashes when trying to show values for positions past the end of the hireImageList size.
override fun onBindViewHolder(holder: HiresViewHolder, position: Int) {
holder.tvClientName.text = nameList[position]
holder.tvClientDetails.text = clientDetailsList[position]
holder.imageView.setImageResource(hireImageList[position])
}
override fun getItemCount(): Int {
return nameList.size
}
Solution
I recommend you 1) learn to read the Logcat tab to find errors, and 2) define a data class to hold the three items you need in your adapter, so you can pass in a single list of that data class instead of three lists. That way there is no way to pass in lists with mis-matched lengths.
For example, use something like this and have the adapter take a single List<HiresAdapter.Data> instead of three lists.
class HiresAdapter(
private var adapterData: ArrayList<Data>,
private var context: Context) : RecyclerView.Adapter<HiresAdapter.HiresViewHolder>() {
// Define a data class in the adapter to hold the data
// needed for each row
data class Data(val name: String, val details: String, val image: Int)
class HiresViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
var tvClientName : TextView = itemView.findViewById(R.id.tvClientName)
var tvClientDetails : TextView = itemView.findViewById(R.id.tvClientDetail)
var imageView : CircleImageView = itemView.findViewById(R.id.hireImageView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : HiresViewHolder {
this.context = parent.context
val view : View = LayoutInflater.from(this.context).inflate(R.layout.rv_card_design,parent, false)
return HiresViewHolder(view)
}
override fun onBindViewHolder(holder: HiresViewHolder, position: Int) {
holder.tvClientName.text = adapterData[position].name
holder.tvClientDetails.text = adapterData[position].details
holder.imageView.setImageResource(adapterData[position].image)
}
override fun getItemCount(): Int {
return adapterData.size
}
}
and populate it like this
val adapterData = ArrayList<HiresAdapter.Data>()
adapterData.add(HiresAdapter.Data("Geoff", "Geoff", R.drawable.solo))
adapterData.add(HiresAdapter.Data("Frogme", "Frogme", R.drawable.tandem))
adapterData.add(HiresAdapter.Data("Jenny", "Jenny", R.drawable.quattro))
adapterData.add(HiresAdapter.Data("Benny", "Benny", R.drawable.sup))
adapterData.add(HiresAdapter.Data("Sylvia", "Sylvia", R.drawable.croc))
adapter = HiresAdapter(adapterData, this#MainActivity)
I have a ViewPager2 in my app that gets populated with fragments dynamically based on a response from a websocket request. I also have a AppCompatImageView that has an image set in the XML layout(but later gets a new bitmap to display dynamically).
Now I have a problem that the ViewPager2 and the AppCompatImageView does not show when the app starts, the only way to show them is to force a focus change like opening a popupmenu or alertdialog.
The really weird thing is that I have another imageview in the layout that is set to a static color that is always shown...
Can someone give me a suggestion why those two views gets hidden (like if they were set to View.INVISIBLE, even though they aren't) on app launch, and even better, why do they get displayed after a focus change?
Could the fact that they get populated dynamically interfere in some way with them being rendered correctly?
I realized that my problems was because I had two race conditions.
The first was due to this code:
class ViewPagerFragmentAdapter(fragmentActivity: FragmentActivity) : FragmentStateAdapter(fragmentActivity) {
private var arrayList: MutableList<Fragment> = mutableListOf()
fun addFragment(fragment: Fragment) {
arrayList.add(fragment)
}
fun removeFragment(index: Int) {
arrayList.removeAt(index)
}
override fun getItemCount(): Int {
return arrayList.size
}
override fun createFragment(position: Int): Fragment {
return arrayList[position]
}
}
This is the adapter for the ViewPager2 and to solve it I hade to change the addFragment function to:
fun addFragment(fragment: Fragment) {
arrayList.add(fragment)
this.notifyItemInserted(this.itemCount - 1)
}
This due to the fact that if my view rendered after my websocket request finished then I got content but if it rendered before I got the reply I would only have an empty viewpager. By adding the notify call the viewpager gets notified when a fragment is added.(probably need something similar on the remove function as well)
To solve the AppCompatImageView I only changed app:srcCompat="..." in the XML to android:src="...", don't ask me why but then the view rendered...
My app context so you can understand my question:
I am currently coding a very complex application with a huge dashboard
that contains a huge list of nested RecyclerViews (both horizontal and
vertical). I know that the vertical RV inside a vertical RV is a bad
practice but unfortunately here the management and client request is
stronger than our advices and they really want it implemented. How did we get to vertical RVs in vertical RV? Our backend offers basically an arrayList of groups that need to be rendered inside our dashboard. Basically a server-driven-ui. These groups contain all sorts of different designs, vertical sub-lists, horizontal sub-lists etc. I have managed to build a prototype for this DashBoard adapter so far and now I want to dive deeper inside the performance optimizations.
Things I have tried to improve performance:
I have implemented DiffUtil to make the dashboard update more
efficiently on a data refresh. I have set the adapter to have
stableIDs using and unique hash that every group has. I have created
flat UI with constraintLayout with absolute minimum of layouts
possible. I have used data binding that was proved to actually improve
the performance just a little, not only better clean code. I am not
doing absolutely any data processing in the adapter or viewHolders,
the entire data flow comes with a final shape from higher data layers.
I have set linearLayoutManager.initialPrefetchItemCount = x on every
nested RVs where x is the number of items rendered on screen before
any scroll.
Then I came across the RecycledViewPool. I have seen some examples of people increasing the performance of the lists with this trick. Basically they get the recyclerViewPool of the parent big adapter and they set it on the nested RVs. I have implemented this as well and my big problem is that now all my nested items get completely mixed up.
Instead of having three horizontal RVs like this:
now I have them all mixed up like this:
Here is my recycledViewPool implementation:
class GlobalAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val groups: ArrayList<Group> = ArrayList()
private var recyclerViewPool = RecyclerView.RecycledViewPool()
[...]
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
CATEGORIES_LIST.typeText.hashCode() -> {
val holder = CategoryGroupViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.group_item_categories_list, parent, false
), adapterCallback
)
holder.binding.recyclerView.setRecycledViewPool(recyclerViewPool)
holder
}
[...]
So I'm basically setting the viewPool to the nested RVs in onCreateViewHolder. On the top of that, following this article: https://medium.com/#thagikura/reduce-the-number-of-inflation-of-viewholders-drastically-by-sharing-a-viewpool-across-multiple-249d5fc6d28 I have also done the following thing in the viewholder:
class CategoryGroupViewHolder(
val binding: GroupItemCategoriesListBinding,
private val adapterCallback: adapterCallback?
):
RecyclerView.ViewHolder(binding.root) {
private val categoryAdapter = CategoryAdapter()
private val defaultItemAnimator = DefaultItemAnimator()
private val linearLayoutManager = LinearLayoutManager(
binding.root.context, LinearLayoutManager.HORIZONTAL, false
)
fun bind(group: Group) = with(binding.root) {
binding.categoryGroup = group
binding.executePendingBindings()
linearLayoutManager.initialPrefetchItemCount = 5
binding.recyclerView.apply {
adapter = categoryAdapter
linearLayoutManager.recycleChildrenOnDetach = true
layoutManager = linearLayoutManager
itemAnimator = defaultItemAnimator
}
}
Notice linearLayoutManager.recycleChildrenOnDetach = true. What am I doing wrong here and why are my items completely mixing up in the entire adapter. Any advice on how to properly use this recycledViewPool?
Also, if any of you have some advice on how to optimize the vertical recyclerView in vertical recyclerView, that would be very much appreciated. Thanks
If I'm not mistaken, I think using ConcatAdapter here will solve your problem in a much simpler way. If you look for an example, you can look at my repo.
https://github.com/cnrture/ConcatAdapterExample
I am working on a dynamic list which is viewed on RecyclerView. So far it is possible to add and remove elements. Also the user can change the content of every single element through several popUpWindows. However, MyViewHolder class got quite long due to many onClickListeners. So I carried MyRecyclerViewAdapter class in a separate file from the rest of the activity. Now,
Is it a good practice to keep MyViewHolder class long with many click listeners (doing the most of the work inside the Adapter object), or should I retrieve the relevant data from MyRecyclerViewAdapter somehow and do the 'delete, add, edit text' work inside onCreate section?
What are the most efficient, simple and fast solutions to show a totally new and different view when all elements are deleted? I tried VISIBLE, GONE solution but MyAdapter is in a separate file and I don't know how to communicate with the onCreate section to transfer real-time size of the dynamic list.
1/ if your actions are about interact with the item component (add, delete, edit, get content...) in a list your should put the function in Adapter, click on ViewHolder should only give its position in adapter. Solution here : RecyclerView itemClickListener in Kotlin
2/ why use VISIBLE, GONE ? when you delete a item that mean you delete the item in your data list so just reload the view after that, adapter will auto show the remain data
Answers:
Question 1: Kiet Phan's recommendation of carrying the logic part inside onBindViewHolder function was quite helpful.
Question 2: If you have already passed the current context to the primary constructor of the separate class (which is defined in another file), it is possible in Kotlin to reach the content of currentActivity by the code segment: (context as Activity).
So the solution and the general structure of my adapter was like this:
import ...
class MyAdapter (var ctx: Context, var list: ArrayList<SomeObject>)
: RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyAdapter.MyViewHolder {
return MyViewHolder(LayoutInflater.from(ctx).inflate(R.layout.singleListElement, parent,false))
}
override fun getItemCount(): Int {
return list.size
}
override fun onBindViewHolder(holder: MyAdapter.MyViewHolder, position: Int) {
//Do the work here, onClickListeners, if statements etc.
//...
list.remove(index)
if(list.size == 0) {
(ctx as Activity).findViewById<TextView>(R.id.listEmpty).visibility=View.VISIBLE
}
} //Close onBindViewHolder
inner class MyViewHolder (v: View) : RecyclerView.ViewHolder(v){
val someView: Textview
init { someView = v.findViewById(R.id.someViewId) }
}
}
It is not easy to write code into this window. When I press Tab button, browser exits the edit screen and selects another part of this webpage. So the code looks like this. Maybe Copy/Paste would do better.
Improvements and more effective solutions for the empty list case are welcome. Thank you.
Based on my theme I use different layouts in my app, so I cant use DataBinding in my use case because this would clutter my whole code with functions like following:
fun getCVAnswer() : CardView {
return when (ThemeManager.themeStyle()) {
ThemeManager.Style.Modern -> return (binding as MviGameActivityModern).cvAnswer1
ThemeManager.Style.Round -> return (binding as MviGameActivityRound).cvAnswer1
...
}
}
So I want to use an alternative solution, all my styles have the same layouts and same ids, so I can do following that works in all styles:
private val cvAnswer1 by lazy { findViewById<CardView>(R.id.cvAnswer1) }
Question
After screen rotation, I will leak the activity and cvAnswer1 has the wrong view in it (the one from my activity before rotation). Any ideas how to solve this?