Android 在片段之间导航后,Recycleview Selection 停止正常工作

Android Recycleview Selection stops working correctly after navigation between fragments

我为 RecycleView 实现了 SelectionTracker,它工作正常,直到我导航到另一个片段并按下后退按钮。导航后,它停止正常工作,选择后我无法再取消选择项目。

我在 github 上创建了一个示例项目,我可以在那里重现错误: https://github.com/alborozd/RecycleViewSelectionProblem

这是我在该示例项目中的代码:

适配器和 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 }
    }
}

项目查找:

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
    }
}

下面是我如何在我的片段中初始化所有这些:

 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()
            }
        })
    }

重现步骤:

  1. 用回收视图打开第一个片段,尝试 select/deselect 项。一切正常
  2. 转到另一个片段,然后按返回按钮
  3. 再次尝试 select/deselect 个项目,您会发现取消选择不再有效

不要在实时数据的观察器中初始化 adapter。因为Live Data可能会被观察n次,所以如果在里面初始化adapter,adapter会被初始化很多次。

要解决问题,请使用以下代码

    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
}