RecyclerView.Adapter and GridLayoutManager with spanCount great than 4 continuously recreates ViewHolders - android

I am working with GridLayoutManager and I have encountered unexpected RecyclerView's behaviour. If spanCount is greater than 4, the RecyclerView continuously recreates ViewHolders on scrolling.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.layoutManager = GridLayoutManager(this, 7)
recyclerView.adapter = Adapter()
}
private class Adapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
//continuously invokes while scrolling:
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view:View = TextView(parent.context).apply {
text = "Hello!"
}
return object : RecyclerView.ViewHolder(view) {}
}
override fun getItemCount(): Int = 3500
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {}
}
How to fix it and force RecyclerView.Adapter to reuse ViewHolders?

I reproduced your issue with this code (copy-pasteable, no resource files required):
class RecActivity : AppCompatActivity() {
lateinit var recyclerView: RecyclerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
recyclerView = RecyclerView(this)
setContentView(recyclerView)
recyclerView.layoutManager = GridLayoutManager(this, 7)
recyclerView.adapter = Adapter()
}
inner class Adapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var vhCount = 0
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val txtView = AppCompatTextView(this#RecActivity)
txtView.tag = vhCount++.toString()
txtView.gravity = Gravity.CENTER
title = vhCount.toString() // display # of created VHs in title
return object : RecyclerView.ViewHolder(txtView){}
}
override fun getItemCount() = 3500
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
(holder.itemView as TextView).text = "$position (${holder.itemView.tag})"
}
}
}
The problem is there's not enough views to fill out entire rows in the RecycledViewPool. By default there's only 5 items per ViewType, so having wide rows of 7 force creation of more ViewHolders while scrolling. To fix this issue, simply increase size of your RecycledViewPool like so (in onCreate):
recyclerView.layoutManager = GridLayoutManager(this, 7)
recyclerView.adapter = Adapter()
// add line below: 0 is default itemViewType, 14 is two rows of items which should be enough
recyclerView.recycledViewPool.setMaxRecycledViews(0, 14)

Related

View all button to button of recycler view

Initially load 3 items from list after click on view all button load remaining all data to same page in kotlin.
To show 3 items I added limit to show data in adapter class in getItemCount mathod. So now I am able to see only 3 items from list. But not able to do like ad all data on same page on click view all button.
Here is how I have solved the issue.
class MyAdapter(private val maxItems: Int = 3) : RecyclerView.Adapter<MyViewHolder>() {
private val items = mutableListOf<String>()
fun setData(data: List<String>) {
items.clear()
items.addAll(data)
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
return MyViewHolder(view)
}
override fun getItemCount(): Int {
return min(items.size, maxItems)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.bind(items[position])
}
fun updateMaxItems(max: Int) {
maxItems = max
notifyDataSetChanged()
}
}
class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val textView = itemView.findViewById<TextView>(R.id.item_text)
fun bind(text: String) {
textView.text = text
}
}
In your Activity/Fragment where you set up the RecyclerView, create a button "View All" and set its onClickListener
val viewAllButton = findViewById<Button>(R.id.viewAllButton)
viewAllButton.setOnClickListener {
//update the adapter's maxItems to show all items
adapter.maxItems = Int.MAX_VALUE
adapter.notifyDataSetChanged()
}

How to get the current loaded rows/items of a RecyclerView

I have a list of items in my RecyclerView and I need to get the currently loaded items (i.e. not the recycled or off screen items) at a time.
This is may adapter
class MyAdapter(list: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
private var items: List<String> = list
class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val myText = view.findViewById<TextView>(R.id.my_text);
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val listItem: View = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return MyViewHolder(listItem)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
holder.myText.text = items[position]
}
override fun getItemCount() = items.size
}
And RecyclerView:
recyclerView.apply {
layoutManager = LinearLayoutManager(this#MainActivity)
hasFixedSize()
adapter = MyAdapter(listOf("a", "b", "c")) // List is bigger than that
}
How can I do that?
I'd a case that I needed to ensure how many RecyclerView items are currently loaded in order to control a performance issue; so that created this for someone else was looking for an answer.
You need to track the list of loaded & recycled items:
The items are loaded in onBindViewHolder() callback
The items are recycled in onViewRecycled() callback
So, we can create a local list that tracks the current loaded item positions, add new positions in onBindViewHolder() and remove recycled ones in onViewRecycled()
In the below code the currentLoadedPositions tracks the loaded positions, and you can create a method that returns it:
class MyAdapter(list: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {
private var items: List<String> = list
// Tracking the currently loaded items in the RecyclerView
private val currentLoadedPositions: ArrayList<Int> = ArrayList()
fun getLoadedPositions(): ArrayList<Int> {
return currentLoadedPositions
}
class MyViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val myText = view.findViewById<TextView>(R.id.my_text)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val listItem: View = LayoutInflater.from(parent.context)
.inflate(R.layout.list_item, parent, false)
return MyViewHolder(listItem)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = items[position]
holder.myText.text = item
currentLoadedPositions.add(position)
Log.d("LOG_TAG", "onViewRecycled: $currentLoadedPositions")
}
override fun getItemCount() = items.size
override fun onViewRecycled(holder: MyViewHolder) {
currentLoadedPositions.remove(Integer.valueOf(holder.adapterPosition));
Log.d("LOG_TAG", "onViewRecycled: $currentLoadedPositions")
}
}

How do i refresh RecyclerView with SwipeRefreshLayout

I want to refresh my Recycler View, i receive my data by viewModel and pass it for my adapter
so i don’t know how to clear this data and call it again
MainActivity:
class MainActivity : AppCompatActivity() {
private val viewModel: ContatoViewModel = ContatoViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main);
configuraObserver()
viewModel.search()
}
private fun configuraObserver() {
viewModel.contato.observe(this, { data ->
Log.i("API", "Data received")
contato_recyclerview.apply {
layoutManager = LinearLayoutManager(this.context, LinearLayoutManager.VERTICAL, false)
adapter = ContatoAdapter(this.context, data.conteudoResposta)
}
})
}
My Adapter:
class ContatoAdapter(private val context: Context?, private val contatos : List<Contato>) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val view = LayoutInflater.from(context).inflate(R.layout.list_item_contato,parent, false)
return ContatoViewHolder(view)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder as ContatoViewHolder
val contato = contatos.elementAt(position)
holder.bindView(contato)
}
override fun getItemCount(): Int {
return contatos.size
}
if you just want to refresh your data(which you already received) in the recycler view you just need to call notifyDataSetChanged() from your Adapter.
SwipeRefreshLayout is needed when you want to implement pull to refresh, which means you want to trigger the initiation of API call when someone pulls down the screen and then after receiving the data you will pass it to Adapter and notifyDataSetChanged()
For implementing pull to refresh you can follow this Google Doc

Strange animation when removing item from RecyclerView

I have a recyclerView populated by the val microphones: MutableList<Microphone>. There's a button in the row layout to remove that item from the list, which I've managed to get functioning, but the animation is going wrong. It looks like when the recyclerView updates, it removes the last item in the list, then the correct item, and then the whole list below the removed item animates to the correct state.
Here's the relevant bits of the recyclerView adapter:
class RecyclerViewAdapter(
val microphones: MutableList<Microphone>
): RecyclerView.Adapter<RecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val cellForRow = LayoutInflater.from(parent.context).inflate(R.layout.favorites_row, parent, false)
return ViewHolder(cellForRow)
}
override fun getItemCount(): Int {
return microphones.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val microphone = microphones[position]
holder.bind(microphone)
holder.favoritesButton.setOnClickListener {
microphones.remove(microphone)
notifyItemRemoved(position)
}
}
class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) {
val modelText = view.findViewById<TextView>(R.id.model_textView)
val brandText = view.findViewById<TextView>(R.id.brand_textView)
val favoritesButton = view.findViewById<ImageButton>(R.id.btnFavorite)
fun bind(
microphone: Microphone,
) {
modelText.text = microphone.model
brandText.text = microphone.brand
}
}
}
Here's a screen recording of the issue:
https://imgur.com/a/pJt3KwH

How to use recyclerview smothScrollToPosition in a viewpager

I use a recyclerview in a viewpager an when I want to scroll to a position using smothScrollToPosition I see no change in the current page but when I slide I see that the change has been applied to the next or the previous page. I have define a function in the sliderAdapter call scrollTo in this method I call smothScrollToPosition.The method scrollTo is call when the user click on a button in the MainActivity
MainActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val pager = findViewById<ViewPager>(R.id.view_pager)
val adapter = SlideAdapter(this)
pager.adapter = adapter
findViewById<Button>(R.id.button).setOnClickListener {
adapter.scrollTo(30)
}
}
}
SlideAdapter
class SlideAdapter(val context: Context) : PagerAdapter() {
lateinit var recyclerView: RecyclerView
lateinit var viewManager : LinearLayoutManager
fun scrollTo(position: Int) {
viewManager.scrollToPositionWithOffset(position, 0)
}
override fun isViewFromObject(view: View, `object`: Any): Boolean = view == `object` as RelativeLayout
override fun getCount() = 5
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val view = LayoutInflater.from(context)
.inflate(R.layout.slide, container, false) as RelativeLayout
val list = mutableListOf<String>()
for (i in 0..50)
list.add("Element $i")
val viewAdapter = Adapter(context, list)
viewManager = LinearLayoutManager(context)
recyclerView = view.findViewById(R.id.recyclerView)
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = viewAdapter
}
container.addView(view)
return view
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as RelativeLayout)
}
}
IMAGE PAGE 1 I CLICK ON THE BUTTON AND I SEE NO CHANGE
IMAGE PAGE 2 I SLIDE AND SEE THAT THE CHANGE HAS BEEN APPLIED ON THIS PAGE.
I need help please ^_^
From JavaDOC of method scrollToPositionWithOffset:
Note that scroll position change will not be reflected until the next layout call.
If you are just trying to make a position visible, use {#link #scrollToPosition(int)}.
May be here is the problem?

Categories

Resources