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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 01:25:06