You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在不使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.26 09:08:30