简体   繁体   中英

Android Recycleview Selection stops working correctly after navigation between fragments

I implemented SelectionTracker for RecycleView and it works fine until I navigate to another fragment and press the back button. After navigation, it stops working correctly and after selection I can't deselect item anymore.

I created a sample project on github and I can reproduce bug there: https://github.com/alborozd/RecycleViewSelectionProblem

Here is my code from that sample project:

Adapter and ViewHolder:

class MyListAdapter()
    : ListAdapter<MyModel, MyListAdapter.MyItemViewHolder>(DiffCallback()) {

    init {
        setHasStableIds(true)
    }

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    private var tracker: SelectionTracker<String>? = null
    fun setTracker(tracker: SelectionTracker<String>?) {
        this.tracker = tracker
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyItemViewHolder {
        return MyItemViewHolder(
            LayoutInflater.from(parent.context)
                .inflate(R.layout.list_item, parent, false),
            this
        )
    }

    override fun onBindViewHolder(holder: MyItemViewHolder, position: Int) {
        val item = getItem(position)
        holder.bind(item, tracker!!.isSelected(item.id))
    }

    class MyItemViewHolder(itemView: View, private val adapter: MyListAdapter) : RecyclerView.ViewHolder(itemView) {

        private var text: TextView? = null
        private var container: View? = null

        init {
            text = itemView.findViewById(R.id.text)
            container = itemView.findViewById(R.id.itemContainer)
        }

        fun bind(item: MyModel, selected: Boolean) {
            text?.text = item.name

            if (selected) {
                val theme = itemView.context!!.theme
                container?.setBackgroundColor(
                    itemView.context!!.resources.getColor(
                        android.R.color.darker_gray,
                        theme
                    )
                )
            } else {
                container?.setBackgroundColor(0)
            }
        }

        fun getItemDetails(): ItemDetailsLookup.ItemDetails<String> =
            object : ItemDetailsLookup.ItemDetails<String>() {
                override fun getPosition(): Int = adapterPosition
                override fun getSelectionKey(): String? = adapter.getItem(adapterPosition).id
                override fun inSelectionHotspot(e: MotionEvent): Boolean {
                    return true
                }
            }
    }

    class DiffCallback : DiffUtil.ItemCallback<MyModel>() {
        override fun areItemsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: MyModel, newItem: MyModel): Boolean {
            return oldItem == newItem
        }
    }
}

ItemIdKeyProvider:

class ItemIdKeyProvider(
    private val adapter: MyListAdapter
) : ItemKeyProvider<String>(SCOPE_MAPPED) {

    override fun getKey(position: Int): String? {
        return adapter.currentList[position].id
    }

    override fun getPosition(key: String): Int {
        return adapter.currentList.indexOfFirst { c -> c.id == key }
    }
}

ItemLookup:

class ItemLookup(private val rv: RecyclerView) : ItemDetailsLookup<String>() {
    override fun getItemDetails(event: MotionEvent)
            : ItemDetails<String>? {

        val view = rv.findChildViewUnder(event.x, event.y)
        if (view != null) {
            return (rv.getChildViewHolder(view) as MyListAdapter.MyItemViewHolder)
                .getItemDetails()
        }
        return null
    }
}

And here is how I initialize all of this in my fragment:

 override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View {

        viewModel = createViewModel()
        binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
        binding.viewModel = viewModel
        binding.lifecycleOwner = this

        viewModel.initViewModel()

        viewModel.items.observe(this, Observer { items ->
            val adapter = MyListAdapter()
            adapter.submitList(items)
            binding.recycleView.adapter = adapter

            trackSelectedItems(adapter, binding.recycleView)
            adapter.notifyDataSetChanged()
        })

        binding.btnGoToNextFragment.setOnClickListener {
            val action = MainFragmentDirections.actionMainFragmentToOtherFragment()
            findNavController().navigate(action)
        }

        return binding.root
    }

    private fun trackSelectedItems(
        adapter: MyListAdapter,
        recyclerView: RecyclerView
    ) {
        tracker = SelectionTracker.Builder<String>(
            "selectionTracker",
            recyclerView,
            ItemIdKeyProvider(adapter),
            ItemLookup(recyclerView),
            StorageStrategy.createStringStorage()
        ).withSelectionPredicate(SelectionPredicates.createSelectAnything())
            .build()

        adapter.setTracker(tracker)

        tracker.addObserver(object : SelectionTracker.SelectionObserver<String>() {
            override fun onSelectionChanged() {
                super.onSelectionChanged()
            }
        })
    }

Steps to reproduce:

  1. Open first fragment with recycleview, try to select/deselect items. Everything works fine
  2. Go to another fragment, then press back button
  3. Try to select/deselect items again and you'll see that deselection doesn't work anymore

Don't initialize adapter inside live data's observer. Because Live Data might be observed n number of times, so if you initialize adapter inside that, adapter will be initialized many times.

To resolve issue use below code

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View {

    viewModel = createViewModel()
    binding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
    binding.viewModel = viewModel
    binding.lifecycleOwner = this

    viewModel.initViewModel()
    val adapter = MyListAdapter()
    binding.recycleView.adapter = adapter
    trackSelectedItems(adapter, binding.recycleView)
    //adapter.notifyDataSetChanged()
    viewModel.items.observe(this, Observer { items ->

        adapter.submitList(items)
    })

    binding.btnGoToNextFragment.setOnClickListener {
        val action = MainFragmentDirections.actionMainFragmentToOtherFragment()
        findNavController().navigate(action)
    }

    return binding.root
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM