Android RecyclerView+Firebase实现菜谱收藏功能死循环问题
问题根因
你遇到的无限循环、按钮自动点击问题是四个代码逻辑错误共同导致的:
- 第一,
addValueEventListener是永久持续监听而非单次读取:你把这个监听器注册在了按钮点击事件、onBindViewHolder生命周期方法里,每次点击、每次列表重绘/条目复用都会新增一个重复的监听器,Firebase数据一变化所有监听器同时触发,反复执行图标切换、数据库写入逻辑,直接触发死循环。 - 第二,
onBindViewHolder中注册的收藏状态监听器逻辑不全:只处理了「收藏存在时设为选中图标」的分支,没处理「收藏被删除时切回未选中图标」的逻辑,加上监听器从不销毁,列表滚动复用时会直接出现状态错乱。 - 第三,靠Drawable的
constantState比对判断选中状态本身不可靠:不同系统版本、Drawable加载场景下这个值可能不一致,很容易出现判断分支走反的问题。 - 额外问题:RecyclerView的
onBindViewHolder会在列表滚动、条目复用时反复执行,你把Firebase实例初始化、监听器注册全写在这里,会造成大量内存泄漏,重复监听器越积越多,卡顿和异常触发概率会随使用时长直线上升。
修复方案
结构调整
- 把Firebase相关实例的初始化移到Adapter构造方法里,不要放在
onBindViewHolder中反复创建。 - 给ViewHolder加布尔类型的标记位存当前条目的收藏状态,不要靠比对Drawable判断选中状态。
- 用
HashMap存每个条目绑定的监听器,在onViewRecycled回收条目时主动移除监听器,避免内存泄漏和重复触发。 - 计数更新不要用「读一次再写回」的逻辑,改用Firebase事务更新,避免高并发场景下计数不准。
- 收藏节点不要存空字符串,存布尔值更省空间,判断存在性也更方便。
修复后的核心实现代码
public class RecipeAdapter extends RecyclerView.Adapter<RecipeAdapter.RecipeViewHolder> { FirebaseAuth mAuth; FirebaseUser mUser; DatabaseReference favoriteRef; DatabaseReference favoriteCountRef; Context context; public List<Recipe> recipes; // 存储每个条目绑定的监听器,回收时移除 private final HashMap<Integer, ValueEventListener> listenerMap = new HashMap<>(); public RecipeAdapter(Context context, List<Recipe> courses) { this.context = context; this.recipes = courses; // Firebase实例初始化移到构造方法,全局只执行一次 mAuth = FirebaseAuth.getInstance(); mUser = mAuth.getCurrentUser(); if (mUser != null) { favoriteRef = FirebaseDatabase.getInstance().getReference().child("favorite").child(mUser.getUid()); favoriteCountRef = FirebaseDatabase.getInstance().getReference().child("count2").child(mUser.getUid()); } } @NonNull @Override public RecipeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View recipeItems = LayoutInflater.from(context).inflate(R.layout.recipe_item, parent, false); return new RecipeViewHolder(recipeItems); } @Override public void onBindViewHolder(@NonNull RecipeViewHolder holder, int position) { Recipe currentRecipe = recipes.get(position); // 绑定前先移除该位置旧的监听器,避免重复 if (listenerMap.containsKey(position)) { favoriteRef.child(currentRecipe.id).removeEventListener(listenerMap.get(position)); } // 注册当前条目的收藏状态监听 ValueEventListener stateListener = new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { holder.isFavorited = snapshot.exists(); holder.saveBtn.setImageResource(holder.isFavorited ? R.drawable.save2 : R.drawable.save); } @Override public void onCancelled(@NonNull DatabaseError error) {} }; favoriteRef.child(currentRecipe.id).addValueEventListener(stateListener); listenerMap.put(position, stateListener); // 收藏按钮点击逻辑 holder.saveBtn.setOnClickListener(v -> { if (holder.isFavorited) { // 已收藏:执行取消 favoriteRef.child(currentRecipe.id).removeValue(); // 事务更新计数,避免并发错误 favoriteCountRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData currentData) { Integer count = currentData.getValue(Integer.class); currentData.setValue(count == null ? 0 : Math.max(0, count - 1)); return Transaction.success(currentData); } @Override public void onComplete(@Nullable DatabaseError error, boolean committed, @Nullable DataSnapshot currentData) {} }); } else { // 未收藏:执行添加 favoriteRef.child(currentRecipe.id).setValue(true); // 事务更新计数 favoriteCountRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData currentData) { Integer count = currentData.getValue(Integer.class); currentData.setValue(count == null ? 1 : count + 1); return Transaction.success(currentData); } @Override public void onComplete(@Nullable DatabaseError error, boolean committed, @Nullable DataSnapshot currentData) {} }); } }); // 原有条目点击跳转逻辑保留 holder.itemView.setOnClickListener(view -> { Intent intent = new Intent(context, RecipePage.class); ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( (Activity) context, new Pair<View, String>(holder.recipeImage, "recipeImage") ); intent.putExtra("recipeImage", currentRecipe.getPhotoLink()); intent.putExtra("recipeTitle", currentRecipe.getTitle()); intent.putExtra("recipeCookTime", currentRecipe.getCookTime()); intent.putExtra("recipeServes", currentRecipe.getServes()); intent.putExtra("recipePrepTime", currentRecipe.getPrepTime()); intent.putExtra("recipeAccess", currentRecipe.getAccess()); intent.putExtra("recipeDesc", currentRecipe.getDescription()); intent.putExtra("recipeIngr", currentRecipe.getIngredients()); intent.putExtra("recipeDire", currentRecipe.getDirections()); context.startActivity(intent, options.toBundle()); }); } @Override public void onViewRecycled(@NonNull RecipeViewHolder holder) { int position = holder.getBindingAdapterPosition(); // 条目回收时移除对应监听器,防止内存泄漏和逻辑误触发 if (position != RecyclerView.NO_POSITION && listenerMap.containsKey(position)) { favoriteRef.child(recipes.get(position).id).removeEventListener(listenerMap.get(position)); listenerMap.remove(position); } super.onViewRecycled(holder); } @Override public int getItemCount() { return recipes.size(); } public static final class RecipeViewHolder extends RecyclerView.ViewHolder { public ImageView recipeImage, saveBtn; TextView username, time, title; // 用布尔值存储收藏状态,替代不可靠的Drawable比对 boolean isFavorited = false; public RecipeViewHolder(@NonNull View itemView) { super(itemView); saveBtn = itemView.findViewById(R.id.saveBtn); recipeImage = itemView.findViewById(R.id.recipeImage); username = itemView.findViewById(R.id.username); username.setPaintFlags(username.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); time = itemView.findViewById(R.id.time); title = itemView.findViewById(R.id.recipeTitle); } } }
内容的提问来源于stack exchange,提问作者user15821113
相关产品推荐
相关产品推荐

