h.joa
h.joa

Reputation: 43

Save Scroll Positions of Nested RecyclerViews

I'm using navigation component in my app and for get data from api i'm using retrofit in MVVM architecture, i want get data from api and display in nested RecyclerView, this is worked and not problem for display data into nested Recylerview but when go to fragment detail and back to previous fragment not saved state and position item in horizontal list ,how to can display current position RecyclerView when back to previous fragment?

parent adapter

import kotlinx.android.synthetic.main.item_main_shaping_group.view.*


class MainShapingAdapter(
private val listGroup: MutableList<MainModel>,
private val listener: ListItemClick
) : RecyclerView.Adapter<MainShapingAdapter.MyViewHolder>(),
MainShapingChildAdapter.ListItemClickChild {

private val viewPool = RecyclerView.RecycledViewPool()


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {

    val layout = LayoutInflater.from(parent.context)
        .inflate(R.layout.item_main_shaping_group, parent, false)
    return MyViewHolder(layout)

}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {


    holder.itemView.apply {
        tv_titleGroup_itemGroup.text = listGroup[position].category.categoryTitle


        rv_itemGroup.layoutManager =
            LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, true)
        rv_itemGroup.adapter = MainShapingChildAdapter(
            listGroup[position].listProduct.toMutableList(),
            this@MainShapingAdapter
        )
        rv_itemGroup.isNestedScrollingEnabled = false
        rv_itemGroup.setRecycledViewPool(viewPool)
        btn_more_itemGroup.setOnClickListener {
            listener.itemOnclickCategory(listGroup[position].category)
        }

    }


}

override fun getItemCount(): Int = listGroup.size

class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

}


interface ListItemClick {
    fun itemOnclickCategory(category: CategoryModel)
    fun itemOnclickChild(product: Product)
}

override fun childOnclick(product: Product) {

    listener.itemOnclickChild(product)
}

override fun onViewRecycled(holder: MyViewHolder) {

    super.onViewRecycled(holder)
    Log.d(ConstantApp.TAG, "onViewRecycled 1")

}


}

childe adapter

import kotlinx.android.synthetic.main.item_main_shaping_child.view.*


  class MainShapingChildAdapter(
  private val listProduct: MutableList<Product>,
  private val listener: ListItemClickChild
) : RecyclerView.Adapter<MainShapingChildAdapter.MyViewHolder>() {


class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {

    val layout = LayoutInflater.from(parent.context)
        .inflate(R.layout.item_main_shaping_child, parent, false)

    return MyViewHolder(layout)

}

override fun onBindViewHolder(holder: MyViewHolder, position: Int) {

    holder.itemView.apply {

        Glide.with(context).load(listProduct[position].productCover)
            .into(iv_coverProduct_shapingChild)
        tv_titleProduct_shapingChild.text = listProduct[position].productTitle
        tv_priceProduct_shapingChild.text = listProduct[position].productPrice.toString()
        setOnClickListener {
            listener.childOnclick(listProduct[position])
        }
    }


}

override fun getItemCount(): Int = listProduct.size


interface ListItemClickChild {
    fun childOnclick(product: Product)
}


 }

Upvotes: 4

Views: 2050

Answers (2)

Letrix
Letrix

Reputation: 3

What worked for me, while using this (Ruben Sousa's blog entry), was using a view model to store the bundle and use it on onDestroyView with scrollStateHolder?.onSaveInstanceState(viewModel.bundle) and on onCreateView with scrollStateHolder = ScrollStateHolder(viewModel.bundle). Just replaced outBundle and savedInstanceState with those and it's working while changing fragment and/or rotation.

I used his ParentAdapter and ChildAdapter with his ScrollStateHolder modified with my models and views and it's working well. Later I'll try with other types of adapters and multi-views.

You could also try, a little more "ugly way" of doing it, create the layout managers that will be used in the child adapters in your fragment and past them when to their respective instances. Then, with the method described before, save and restore theor instance state. [not tested]

Upvotes: 0

Lheonair
Lheonair

Reputation: 498

I used this tutorial to make my carousel recycler views hold their scroll state: https://rubensousa.com/2019/08/27/saving_scroll_state_of_nested_recyclerviews/

Basically you have to create a new class:

import android.os.Bundle
import android.os.Parcelable
import androidx.recyclerview.widget.RecyclerView

/**
 * Persists scroll state for nested RecyclerViews.
 *
 * 1. Call [saveScrollState] in [RecyclerView.Adapter.onViewRecycled]
 * to save the scroll position.
 *
 * 2. Call [restoreScrollState] in [RecyclerView.Adapter.onBindViewHolder]
 * after changing the adapter's contents to restore the scroll position
 */
class ScrollStateHolder(savedInstanceState: Bundle? = null) {

    companion object {
        const val STATE_BUNDLE = "scroll_state_bundle"
    }

    /**
     * Provides a key that uniquely identifies a RecyclerView
     */
    interface ScrollStateKeyProvider {
        fun getScrollStateKey(): String?
    }


    /**
     * Persists the [RecyclerView.LayoutManager] states
     */
    private val scrollStates = hashMapOf<String, Parcelable>()

    /**
     * Keeps track of the keys that point to RecyclerViews
     * that have new scroll states that should be saved
     */
    private val scrolledKeys = mutableSetOf<String>()

    init {
        savedInstanceState?.getBundle(STATE_BUNDLE)?.let { bundle ->
            bundle.keySet().forEach { key ->
                bundle.getParcelable<Parcelable>(key)?.let {
                    scrollStates[key] = it
                }
            }
        }
    }

    fun setupRecyclerView(recyclerView: RecyclerView, scrollKeyProvider: ScrollStateKeyProvider) {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    saveScrollState(recyclerView, scrollKeyProvider)
                }
            }

            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView, dx, dy)
                val key = scrollKeyProvider.getScrollStateKey()
                if (key != null && dx != 0) {
                    scrolledKeys.add(key)
                }
            }
        })
    }

    fun onSaveInstanceState(outState: Bundle) {
        val stateBundle = Bundle()
        scrollStates.entries.forEach {
            stateBundle.putParcelable(it.key, it.value)
        }
        outState.putBundle(STATE_BUNDLE, stateBundle)
    }

    fun clearScrollState() {
        scrollStates.clear()
        scrolledKeys.clear()
    }

    /**
     * Saves this RecyclerView layout state for a given key
     */
    fun saveScrollState(
        recyclerView: RecyclerView,
        scrollKeyProvider: ScrollStateKeyProvider
    ) {
        val key = scrollKeyProvider.getScrollStateKey() ?: return
        // Check if we scrolled the RecyclerView for this key
        if (scrolledKeys.contains(key)) {
            val layoutManager = recyclerView.layoutManager ?: return
            layoutManager.onSaveInstanceState()?.let { scrollStates[key] = it }
            scrolledKeys.remove(key)
        }
    }

    /**
     * Restores this RecyclerView layout state for a given key
     */
    fun restoreScrollState(
        recyclerView: RecyclerView,
        scrollKeyProvider: ScrollStateKeyProvider
    ) {
        val key = scrollKeyProvider.getScrollStateKey() ?: return
        val layoutManager = recyclerView.layoutManager ?: return
        val savedState = scrollStates[key]
        if (savedState != null) {
            layoutManager.onRestoreInstanceState(savedState)
        } else {
            // If we don't have any state for this RecyclerView,
            // make sure we reset the scroll position
            layoutManager.scrollToPosition(0)
        }
        // Mark this key as not scrolled since we just restored the state
        scrolledKeys.remove(key)
    }

}

Then you use this class to store the state when the fragment/activity is detached/destroyed.

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    scrollStateHolder = ScrollStateHolder(savedInstanceState)
    return inflater.inflate(R.layout.layout, container, false)
}

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    scrollStateHolder.onSaveInstanceState(outState)
}

You also have to use these two lines somewhere in your code:

scrollStateHolder.setupRecyclerView(itemView.child_recipes_rv, this)
    scrollStateHolder.restoreScrollState(itemView.child_recipes_rv, this)

I'm saying 'somewhere' because that depends on your specific implementation. I did a variation of what the guy did in the tutorial, so that's up to you. In my case, those two lines are called one after the other when I'm building each child recycler view.

Basically you have to have an identifier for every child recyclerview. And you use that as a key in your map (see ScrollStateHolder.kt), and then when saving the state of the fragment/activity you're saving the state and that includes the scrolling state of the recyclerview.

Upvotes: 1

Related Questions