从Firebase实时数据库删除数据时触发IndexOutOfBoundsException异常
问题背景
Android应用通过RecyclerView展示Firebase Realtime Database中的用户食材,为列表项添加删除功能,使用代码fridgeRef.child(getRef(position).getKey()).removeValue();执行删除操作。
Firebase JSON结构:
{ "Ingredient": { "hgO9joLhmfh4Wjn7xYpyqcYmNOB3": { "Cashews": { "Expiry": "2023-01-21", "Ingredient": 1 }, "Macadamia Nuts": { "Expiry": "2022-11-22", "Ingredient": 1 }, "Pecans": { "Expiry": "2023-01-21", "Ingredient": 1 }, "Pine Nuts": { "Expiry": "2022-11-22", "Ingredient": 1 }, "Pistachios": { "Expiry": "2023-01-21", "Ingredient": 1 } } } }
问题表现
- 删除第一个项
Cashews成功,但删除Macadamia Nuts时会误删下方的Pecans; - 仅剩
Macadamia Nuts时,删除操作抛出索引越界异常导致崩溃:
java.lang.IndexOutOfBoundsException: Index: 4, Size: 3
at java.util.ArrayList.get(ArrayList.java:437)
at com.firebase.ui.common.BaseObservableSnapshotArray.getSnapshot(BaseObservableSnapshotArray.java:70)
at com.firebase.ui.database.FirebaseRecyclerAdapter.getRef(FirebaseRecyclerAdapter.java:114)
at com.example.recipely.fridge$2.lambda$onBindViewHolder$0$com-example-recipely-fridge$2(fridge.java:163)
at com.example.recipely.fridge$2$$ExternalSyntheticLambda0.onClick(Unknown Source:4)
相关代码(fridge.java)
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment ingredientView = inflater.inflate(R.layout.fragment_fridge, container, false); expiryNote = (LinearLayout) ingredientView.findViewById(R.id.expiryNote); HaveData = (ScrollView) ingredientView.findViewById(R.id.HaveData); noFridge = (LinearLayout) ingredientView.findViewById(R.id.noFridge); expiryNote.setVisibility(View.GONE); HaveData.setVisibility(View.GONE); myIngredientList = (RecyclerView) ingredientView.findViewById(R.id.ingredientList); myIngredientList.setLayoutManager(new LinearLayoutManager(getContext())); mAuth = FirebaseAuth.getInstance(); currentUserID = mAuth.getCurrentUser().getUid(); fridgeRef = FirebaseDatabase.getInstance().getReference().child("Ingredient").child(currentUserID); fridgeRef.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { if(snapshot.exists()){ HaveData.setVisibility(View.VISIBLE); } } @Override public void onCancelled(@NonNull DatabaseError error) { } }); return ingredientView; } @Override public void onStart() { super.onStart(); FirebaseRecyclerOptions<fridgeItem> options = new FirebaseRecyclerOptions.Builder<fridgeItem>() .setQuery(fridgeRef , fridgeItem.class) .build(); FirebaseRecyclerAdapter<fridgeItem, fridgeViewHolder> adapter = new FirebaseRecyclerAdapter<fridgeItem, fridgeViewHolder>(options) { @Override protected void onBindViewHolder(@NonNull fridgeViewHolder holder, int position, @NonNull fridgeItem model) { String itemName = getRef(position).getKey(); LocalDate expiry = LocalDate.parse(model.getExpiry()); LocalDate today = LocalDate.now(); long dayDiff = DAYS.between(today, expiry); if(dayDiff <= 7){ holder.cardViewIngredient.setBackgroundColor(Color.parseColor("#FFD1D1")); expiryNote.setVisibility(View.VISIBLE); } holder.ingredName.setText(itemName); holder.ingredExpiry.setText(dayDiff + " Days until expiry"); holder.deleteBtn.setOnClickListener(v ->{ String output = fridgeRef.child(getRef(position).getKey()).toString(); Log.d("Fridge" , output ); fridgeRef.child(getRef(position).getKey()).removeValue(); }); } @NonNull @Override public fridgeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.ingredientrecycle, parent, false); fridgeViewHolder viewHolder = new fridgeViewHolder(view); return viewHolder; } }; myIngredientList.setAdapter(adapter); adapter.startListening(); } public static class fridgeViewHolder extends RecyclerView.ViewHolder{ TextView ingredName, ingredExpiry; LinearLayout deleteBtn, cardViewIngredient; public fridgeViewHolder(@NonNull View itemView) { super(itemView); ingredName = itemView.findViewById(R.id.itemName); ingredExpiry = itemView.findViewById(R.id.itemExpiry); cardViewIngredient = itemView.findViewById(R.id.cardViewIngredient); //set button to delete an item deleteBtn = itemView.findViewById(R.id.deleteItem); } } }
问题原因
删除按钮点击事件中直接使用了onBindViewHolder传入的position参数。RecyclerView的position是动态变化的:当列表项被删除后,Adapter会更新数据集,但之前绑定的点击事件中保留的是旧的索引值,导致后续删除时获取错误的getRef(position),进而误删数据或触发索引越界异常。
解决方案
方案一:使用getAdapterPosition()获取实时位置
修改删除按钮的点击事件,通过ViewHolder获取当前最新的有效位置:
holder.deleteBtn.setOnClickListener(v ->{ int currentPosition = holder.getAdapterPosition(); if(currentPosition != RecyclerView.NO_POSITION){ // 确保位置有效 String itemKey = getRef(currentPosition).getKey(); fridgeRef.child(itemKey).removeValue(); } });
方案二:提前保存食材Key到ViewHolder Tag
在绑定视图时,将当前食材的Key保存到ViewHolder的Tag中,点击时直接取出使用,完全脱离对position的依赖:
// 在onBindViewHolder方法中添加 String itemKey = getRef(position).getKey(); holder.itemView.setTag(itemKey); // 修改删除点击事件 holder.deleteBtn.setOnClickListener(v ->{ String targetKey = (String) holder.itemView.getTag(); fridgeRef.child(targetKey).removeValue(); });
额外优化:过期提示的动态控制
原代码中每次绑定过期食材都会显示expiryNote,但所有过期食材被删除后无法自动隐藏。可以在Adapter中重写onDataChanged方法,统一检查数据集状态:
@Override public void onDataChanged() { super.onDataChanged(); boolean hasExpiredItem = false; for(int i = 0; i < getItemCount(); i++){ fridgeItem item = getItem(i); LocalDate expiry = LocalDate.parse(item.getExpiry()); long dayDiff = DAYS.between(LocalDate.now(), expiry); if(dayDiff <= 7){ hasExpiredItem = true; break; } } expiryNote.setVisibility(hasExpiredItem ? View.VISIBLE : View.GONE); }
内容的提问来源于stack exchange,提问作者Muhammad Zarith

