MVVM模式下,ViewHolder中观察LiveData加载聊天最后消息是否正确?
问题分析与解决方案
你的这种在ViewHolder中观察LiveData的方式存在明显问题,主要有以下几点:
- ViewHolder是会被复用的,当同一个ViewHolder绑定不同位置的聊天项时,之前设置的LiveData观察者没有被移除,会导致旧的回调触发,造成UI错乱(比如显示上一个项的消息)。
- 每次
onBindViewHolder都调用getLastMessageWithProfile,很容易重复发起网络请求(比如列表滚动时ViewHolder复用,同一个ProfileID可能被多次请求),浪费资源。 - ViewHolder持有LiveData并绑定生命周期,可能引发内存泄漏,因为ViewHolder的生命周期和页面的生命周期并不完全匹配。
MVVM模式下的正确实现方式
核心原则:ViewHolder只负责UI绑定,数据的获取、状态管理全部交给ViewModel,Adapter只做数据到UI的映射。具体步骤如下:
1. 定义统一的列表数据模型
先创建一个包含所有所需信息的数据类,替代原来的ChatConversation,把加载状态也包含进去:
data class ChatItem( val profileId: String, val userProfile: UserProfile? = null, // 用户资料 val lastMessage: Message? = null, // 最后一条消息 val loadStatus: LoadStatus = LoadStatus.LOADING // 加载状态:加载中/成功/失败 ) enum class LoadStatus { LOADING, SUCCESS, ERROR }
2. ViewModel层管理数据与加载逻辑
ViewModel要负责:初始化列表、按需加载可见项的资料和消息、缓存请求结果、更新数据状态。
class ChatsViewModel : ViewModel() { // 列表的核心数据源,Adapter观察这个LiveData private val _chatItems = MutableLiveData<List<ChatItem>>() val chatItems: LiveData<List<ChatItem>> = _chatItems // 缓存已加载的资料和消息,避免重复请求 private val loadedProfiles = mutableMapOf<String, UserProfile>() private val loadedLastMessages = mutableMapOf<String, Message>() // 初始化:传入服务器返回的ProfileIDs列表 fun initChatList(profileIds: List<String>) { val initialItems = profileIds.map { ChatItem(profileId = it) } _chatItems.value = initialItems } // 加载指定位置的聊天项数据(当Item可见时调用) fun loadChatItemData(position: Int) { val currentItems = _chatItems.value ?: return val item = currentItems[position] // 如果已经加载过,直接返回 if (item.loadStatus == LoadStatus.SUCCESS) return // 并行请求用户资料和最后消息 viewModelScope.launch { val profileDeferred = async { fetchUserProfile(item.profileId) } val messageDeferred = async { fetchLastMessage(item.profileId) } val profile = profileDeferred.await() val message = messageDeferred.await() // 更新缓存 profile?.let { loadedProfiles[item.profileId] = it } message?.let { loadedLastMessages[item.profileId] = it } // 更新列表数据 val updatedItems = currentItems.mapIndexed { idx, chatItem -> if (idx == position) { chatItem.copy( userProfile = profile, lastMessage = message, loadStatus = if (profile != null && message != null) LoadStatus.SUCCESS else LoadStatus.ERROR ) } else { chatItem } } _chatItems.value = updatedItems } } // 封装的API请求方法 private suspend fun fetchUserProfile(profileId: String): UserProfile? { // 调用用户资料API return try { apiService.getUserProfile(profileId) } catch (e: Exception) { null } } private suspend fun fetchLastMessage(profileId: String): Message? { // 调用最后消息API return try { apiService.getLastMessage(profileId) } catch (e: Exception) { null } } }
3. Adapter层只做UI绑定
Adapter观察ViewModel的chatItems LiveData,用DiffUtil做高效更新,onBindViewHolder只负责把数据绑定到UI,同时触发ViewModel加载可见项的数据:
class ChatsAdapter(private val viewModel: ChatsViewModel) : RecyclerView.Adapter<ChatsViewHolder>() { private var chatItems = emptyList<ChatItem>() init { // 观察ViewModel的数据变化,用DiffUtil更新列表 viewModel.chatItems.observeForever { newItems -> val diffResult = DiffUtil.calculateDiff(ChatItemDiffCallback(chatItems, newItems)) chatItems = newItems diffResult.dispatchUpdatesTo(this) } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChatsViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_chat, parent, false) return ChatsViewHolder(view) } override fun onBindViewHolder(holder: ChatsViewHolder, position: Int) { val item = chatItems[position] holder.bind(item) // 当Item绑定(即进入可见区域)时,触发ViewModel加载数据 viewModel.loadChatItemData(position) } override fun getItemCount(): Int = chatItems.size // DiffUtil回调,用于高效更新列表 class ChatItemDiffCallback( private val oldList: List<ChatItem>, private val newList: List<ChatItem> ) : DiffUtil.Callback() { override fun getOldListSize(): Int = oldList.size override fun getNewListSize(): Int = newList.size override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition].profileId == newList[newItemPosition].profileId } override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { return oldList[oldItemPosition] == newList[newItemPosition] } } } // ViewHolder只负责绑定UI,无任何数据逻辑 class ChatsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bind(item: ChatItem) { when(item.loadStatus) { LoadStatus.LOADING -> { // 显示加载中UI itemView.tv_last_message.text = "加载中..." } LoadStatus.SUCCESS -> { // 显示用户资料和最后消息 itemView.tv_username.text = item.userProfile?.name itemView.tv_last_message.text = item.lastMessage?.content } LoadStatus.ERROR -> { // 显示加载失败UI itemView.tv_last_message.text = "加载失败" } } } }
4. 监听列表滚动(可选优化)
如果不想每次onBindViewHolder都触发请求(比如快速滚动时频繁触发),可以用RecyclerView.OnScrollListener结合LinearLayoutManager.findFirstVisibleItemPosition()和findLastVisibleItemPosition(),只对可见区域内的Item发起请求,减少不必要的调用。
关键总结
- ViewHolder的职责仅限UI绑定,不能处理数据请求或LiveData观察,否则会引发复用问题和内存泄漏。
- ViewModel是数据的唯一管理者,负责数据的加载、缓存、状态更新,所有数据变化通过LiveData通知UI。
- Adapter作为ViewModel和ViewHolder的中间层,只负责数据的分发和列表的高效更新。
内容的提问来源于stack exchange,提问作者Giru Bhai
相关产品推荐
相关产品推荐

