airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.46k stars 730 forks source link

How to keep view state when scrolling? #1303

Open syb6962 opened 1 year ago

syb6962 commented 1 year ago

I made an expandable nested recyclerview using epoxy.

However, there was a problem that the app crashed when scrolling down and up.(error : #1115)

This error was resolved as follows by referring to the here

override fun buildModels() {
        data.forEachIndexed { index, s ->
            ParentDataModel_()
                .id(index)
                .title(s.workout)
                .list(s.list)
                .onBind { model, view, position -> 
                    // just call requestModelBuild() 
                    requestModelBuild()
                }
                .addTo(this)

        }
    }

But now we have another new problem.

If an item is scrolled while it is expanded, it will not be maintained.

expanded state KakaoTalk_20220710_051230816_02

scrolled and return KakaoTalk_20220710_051230816_03

As in the image, it is scrolled in an expanded state, but the first item is in the default state.

How do I solve it?


@EpoxyModelClass(layout = R.layout.item_data)
abstract class ParentDataModel : EpoxyModelWithHolder<ParentDataModel.ItemViewHolder>() {

    @EpoxyAttribute
    lateinit var title: String

    @EpoxyAttribute
    lateinit var list: List<String>

    @EpoxyAttribute
    var expanded: Boolean = false

    override fun bind(holder: ItemViewHolder) {
        super.bind(holder)
        holder.tv.text = title

        val controller = ChildEpoxyController()
        holder.nestedRV.adapter = controller.adapter
        controller.setItem(list)

        holder.expandBtn.setOnClickListener {
            if(expanded) {
                holder.nestedRV.visibility = View.GONE
            }
            else
                holder.nestedRV.visibility = View.VISIBLE
            expanded = !expanded
            controller.requestModelBuild()
        }
    }

    class ItemViewHolder : KotlinEpoxyHolder() {
        val tv by bind<TextView>(R.id.tv)
        val nestedRV by bind<RecyclerView>(R.id.nested_rv)
        val expandBtn by bind<TextView>(R.id.expanded)
    }
}
ardmn commented 1 year ago

I think a cause of problem is that your bind method doesn't use expanded flag to setup view. To solve this problem you should do something like this:

  override fun bind(holder: ItemViewHolder) {
        super.bind(holder)
        holder.tv.text = title
        holder.nestedRV.visibility = expanded
       ......
    }

Epoxy reuse Views and its ViewHolders for RecyclerView items. When app calls to controller.requestModelBuild() epoxy creates new ParentDataModel objects but it doesn't creates new Views. Thats why when user expand one item then scrolls down and expanded view will be outside bound of visibility the epoxy can reuse expanded view to show new item which became visible and this item described by ParentDataModel object can has expanded == false property but view will be expanded because of it was changed by previous item (android's View object was changed). That's why you should do full setup of you view in 'bind' method of EpoxyModelWithHolder and don't rely to state of view (Don't think than a. view is just inflated from your xml with default settings),

viroth-ty commented 1 year ago

In order to keep state in model, you need to store properties in data class. Example : class Todo(val id: Int, val expanded: Boolean = false) Every time item clicked, update value to data class. The way you update value to data class is up to you. My suggestion: Update value using android view model. Check official doc viewmodel