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

MVVM+Firebase架构下RecyclerView收藏图标功能异常求助

问题根源分析
  1. 单个LiveData无法区分多商品状态:StoreHomeViewModel用单个_isFavorite LiveData存储所有商品的收藏状态,导致所有商品共享同一状态值,进入详情页返回后状态被覆盖为最后一次请求结果,出现全选异常。
  2. Adapter初始化逻辑错误:在isFavorite LiveData观察者中重复创建Adapter,初始加载时未拿到收藏状态就生成Adapter,导致初始状态全为未选中。
  3. 收藏状态判断逻辑不准确:Repository中用it.result.value == productId判断收藏状态,不如直接使用snapshot.exists()可靠,Firebase节点存在即表示商品已被收藏。
  4. ViewModel中收藏操作逻辑混乱:checkFavorite方法的finally块条件判断逻辑错误,未正确根据用户操作和实际收藏状态执行添加/移除操作。
  5. Adapter未绑定初始收藏状态:ViewHolder绑定仅设置点击监听,未初始化收藏按钮的选中状态,导致初始显示全部为未选中。

解决方案(基于MVVM+整洁架构)

1. 重构ProductRepository

优化收藏状态判断逻辑,新增批量获取收藏列表方法以减少Firebase请求次数:

class ProductRepository(
    val database: FirebaseDatabase,
    val storageRef: StorageReference
) {
    private val userFavoritesRef by lazy {
        database.reference.child("users")
            .child(FirebaseProfileService.getCurrentUserUid()!!)
            .child("favoritesList")
    }

    // 检查单个商品是否收藏
    suspend fun checkFavorite(productId: String): Boolean = withContext(Dispatchers.IO) {
        val deferred = CompletableDeferred<Boolean>()
        userFavoritesRef.child(productId).get().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                deferred.complete(task.result.exists())
            } else {
                Log.d(TAG, "checkFavorite failed", task.exception)
                deferred.complete(false)
            }
        }
        deferred.await()
    }

    // 批量获取用户所有收藏的商品ID(更高效)
    suspend fun getUserFavorites(): List<String> = withContext(Dispatchers.IO) {
        val deferred = CompletableDeferred<List<String>>()
        userFavoritesRef.get().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val favorites = task.result.children.mapNotNull { it.key }
                deferred.complete(favorites)
            } else {
                Log.d(TAG, "getUserFavorites failed", task.exception)
                deferred.complete(emptyList())
            }
        }
        deferred.await()
    }

    // 添加收藏
    suspend fun addFavorite(productId: String) = withContext(Dispatchers.IO) {
        userFavoritesRef.child(productId).setValue(productId)
    }

    // 移除收藏
    suspend fun removeFavorite(productId: String) = withContext(Dispatchers.IO) {
        userFavoritesRef.child(productId).removeValue()
    }

    companion object {
        private const val TAG = "ProductRepository"
    }
}

2. 重构StoreHomeViewModel

使用Map<商品ID, 收藏状态>存储每个商品的独立状态,同时批量加载商品列表和收藏状态:

class StoreHomeViewModel(app: Application, private val repository: ProductRepository) :
    AndroidViewModel(app) {

    // 存储所有商品的收藏状态:key=productId, value=是否收藏
    private val _favoriteStates = MutableLiveData<Map<String, Boolean>>(emptyMap())
    val favoriteStates: LiveData<Map<String, Boolean>> get() = _favoriteStates

    // 商品列表LiveData
    private val _products = MutableLiveData<List<Product>>()
    val products: LiveData<List<Product>> get() = _products

    init {
        loadProductsAndFavorites()
    }

    // 同时加载商品列表和收藏状态
    private fun loadProductsAndFavorites() {
        viewModelScope.launch {
            val favoriteIds = repository.getUserFavorites()
            val productsList = loadProductsFromFirebase()
            _products.value = productsList
            
            // 构建收藏状态映射表
            val states = productsList.associate { product ->
                product.id to favoriteIds.contains(product.id)
            }
            _favoriteStates.value = states
        }
    }

    // 处理收藏按钮切换逻辑
    fun toggleFavorite(productId: String, currentChecked: Boolean) {
        viewModelScope.launch {
            val currentStates = _favoriteStates.value ?: emptyMap()
            val newState = !currentChecked
            
            // 同步到Firebase
            if (newState) {
                repository.addFavorite(productId)
            } else {
                repository.removeFavorite(productId)
            }
            
            // 更新本地状态
            _favoriteStates.value = currentStates + (productId to newState)
        }
    }

    // 替换为实际的商品加载逻辑
    private suspend fun loadProductsFromFirebase(): List<Product> {
        // 从Firebase products节点加载数据
        return emptyList()
    }

    fun onProductClicked(product: Product) {
        // 处理商品点击跳转逻辑
    }
}

3. 修改StoreHomeFragment

仅初始化一次Adapter,通过观察LiveData更新商品列表和收藏状态:

class StoreHomeFragment : Fragment() {

    private val viewModel: StoreHomeViewModel by viewModel()
    private lateinit var adapter: ProductsListAdapter

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 初始化Adapter
        adapter = ProductsListAdapter(
            ProductsListAdapter.ProductClickListener { product ->
                viewModel.onProductClicked(product)
            },
            ProductsListAdapter.FavoriteToggleListener { productId, isChecked ->
                viewModel.toggleFavorite(productId, isChecked)
            }
        )
        binding.recyclerView.adapter = adapter

        // 观察商品列表更新
        viewModel.products.observe(viewLifecycleOwner) { products ->
            adapter.submitList(products)
        }

        // 观察收藏状态更新
        viewModel.favoriteStates.observe(viewLifecycleOwner) { states ->
            adapter.updateFavoriteStates(states)
        }
    }
}

4. 重构ProductsListAdapter

新增收藏状态存储,绑定ViewHolder时设置初始状态并避免重复监听:

class ProductsListAdapter(
    private val clickListener: ProductClickListener,
    private val favoriteToggleListener: FavoriteToggleListener
) : ListAdapter<Product, ProductsListAdapter.ProductsListViewHolder>(DiffCallback()) {

    private var favoriteStates: Map<String, Boolean> = emptyMap()

    // 更新收藏状态并通知刷新
    fun updateFavoriteStates(states: Map<String, Boolean>) {
        favoriteStates = states
        notifyItemRangeChanged(0, itemCount)
    }

    class ProductsListViewHolder(private val binding: ProductListItemBinding) :
        RecyclerView.ViewHolder(binding.root) {

        fun bind(
            product: Product,
            clickListener: ProductClickListener,
            favoriteToggleListener: FavoriteToggleListener,
            isFavorite: Boolean
        ) {
            binding.product = product
            binding.clickListener = clickListener

            // 先移除旧监听避免重复触发,再设置初始状态
            binding.favIcon.setOnCheckedChangeListener(null)
            binding.favIcon.isChecked = isFavorite
            
            // 设置新的切换监听
            binding.favIcon.setOnCheckedChangeListener { _, isChecked ->
                favoriteToggleListener.onToggle(product.id, isChecked)
            }

            binding.executePendingBindings()
        }
    }

    override fun onBindViewHolder(holder: ProductsListViewHolder, position: Int) {
        val product = getItem(position)
        val isFavorite = favoriteStates[product.id] ?: false
        holder.bind(product, clickListener, favoriteToggleListener, isFavorite)
    }

    class ProductClickListener(val clickListener: (product: Product) -> Unit) {
        fun onClick(product: Product) = clickListener(product)
    }

    class FavoriteToggleListener(val toggleListener: (productId: String, isChecked: Boolean) -> Unit) {
        fun onToggle(productId: String, isChecked: Boolean) = toggleListener(productId, isChecked)
    }

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

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

关键优化点

  • 批量请求优化:通过getUserFavorites一次性获取所有收藏ID,减少Firebase调用次数
  • 独立状态管理:用Map<String, Boolean>存储每个商品的收藏状态,避免状态冲突
  • Adapter逻辑简化:仅初始化一次Adapter,通过submitList和updateFavoriteStates分别更新商品数据和收藏状态
  • 监听防重复:绑定ViewHolder时先移除旧监听,避免重复触发状态变更
  • 状态同步一致:ViewModel中先更新本地状态再同步Firebase,确保UI状态与后端数据一致

内容的提问来源于stack exchange,提问作者Mahmoud Reda

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 21:55:15