如何在不使用DataBinding的情况下结合RecyclerView与ViewModel实现可展开列表
不用DataBinding实现RecyclerView条目与ViewModel的结合(带展开/收起功能)
嘿,这个需求我之前帮好几个开发者梳理过,不用DataBinding完全能搞定,而且还能完美贴合Google的架构组件思路,咱们一步步来拆解:
第一步:为单个条目创建专属ViewModel
每个RecyclerView条目有自己的展开/收起状态,所以咱们给每个条目单独搞个ItemViewModel,专门管理这个状态——这完全符合ViewModel“管理UI状态”的定位:
class ItemViewModel : ViewModel() { // 用MutableLiveData存展开状态,默认是收起(false) private val _isExpanded = MutableLiveData(false) // 对外暴露不可变的LiveData,保证状态只能通过ViewModel修改 val isExpanded: LiveData<Boolean> = _isExpanded // 切换展开状态的方法,外部只需要调用这个方法,不用关心内部逻辑 fun toggleExpansion() { _isExpanded.value = _isExpanded.value?.not() } }
第二步:改造RecyclerView Adapter
Adapter需要维护每个条目对应的ViewModel,还要处理ViewHolder的绑定和回收逻辑:
1. 初始化ViewModel列表
在Adapter构造时,给每个数据项对应创建一个ItemViewModel,存在列表里和数据一一对应:
class ExpandableItemsAdapter(private val items: List<YourItemDataModel>) : RecyclerView.Adapter<ExpandableItemsAdapter.ItemViewHolder>() { // 存储每个条目的ViewModel,和数据列表位置一一对应 private val itemViewModels = mutableListOf<ItemViewModel>() init { // 为每个数据项初始化ViewModel repeat(items.size) { itemViewModels.add(ItemViewModel()) } } // 剩下的Adapter方法继续往下写... }
2. 实现ViewHolder的绑定逻辑
在ViewHolder里,我们要做这几件事:绑定数据、观察ViewModel的状态变化、设置点击事件触发状态切换。另外一定要注意移除观察者,不然会有内存泄漏:
inner class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val toggleBtn: Button = itemView.findViewById(R.id.btn_toggle) private val innerRecycler: RecyclerView = itemView.findViewById(R.id.inner_recycler) private val titleTv: TextView = itemView.findViewById(R.id.tv_item_title) // 内部列表的适配器,根据你的需求自定义 private val innerAdapter = YourInnerListAdapter() // 存一下观察者实例,方便回收时移除 private var expansionObserver: Observer<Boolean>? = null init { innerRecycler.adapter = innerAdapter } fun bind(viewModel: ItemViewModel, itemData: YourItemDataModel) { // 1. 绑定基础数据 titleTv.text = itemData.title // 2. 观察展开状态变化,更新UI expansionObserver = Observer { isExpanded -> // 切换内部列表可见性 innerRecycler.visibility = if (isExpanded) View.VISIBLE else View.GONE // 更新按钮文本 toggleBtn.text = if (isExpanded) "显示更少" else "显示更多" } // 用observeForever,因为ViewHolder没有自带LifecycleOwner viewModel.isExpanded.observeForever(expansionObserver!!) // 3. 设置按钮点击事件,调用ViewModel的方法切换状态 toggleBtn.setOnClickListener { viewModel.toggleExpansion() } // 4. 更新内部列表的数据 innerAdapter.submitList(itemData.innerItems) } // 提供移除观察者的方法,在ViewHolder被回收时调用 fun clearObservers(viewModel: ItemViewModel) { expansionObserver?.let { viewModel.isExpanded.removeObserver(it) } expansionObserver = null } }
3. 处理ViewHolder回收
在Adapter里重写onViewRecycled方法,及时移除观察者,避免内存泄漏:
override fun onViewRecycled(holder: ItemViewHolder) { super.onViewRecycled(holder) val position = holder.adapterPosition if (position != RecyclerView.NO_POSITION) { val viewModel = itemViewModels[position] holder.clearObservers(viewModel) } }
第三步:绑定Adapter到RecyclerView
最后就是常规操作,在你的Fragment/Activity里初始化Adapter并设置给RecyclerView就行:
// 假设你已经有了数据列表 val items = getYourItemDataList() val adapter = ExpandableItemsAdapter(items) recyclerView.adapter = adapter recyclerView.layoutManager = LinearLayoutManager(requireContext())
为啥这么做?
- 状态完全由ViewModel管理,符合架构组件的设计思想,避免把状态逻辑写在Adapter或ViewHolder里,代码更清晰。
- 条目被回收再复用的时候,ViewModel的状态会保留,滑动回来时展开状态不会丢失,用户体验更好。
内容的提问来源于stack exchange,提问作者Tima
相关产品推荐
相关产品推荐

