Android ListView自定义Adapter中Firestore查询仅更新部分项问题
ConsignmentAdapter 异步加载Firestore数据列表项错乱修复
实现需求
自定义继承ArrayAdapter<Consignment>的ConsignmentAdapter实现寄件单列表渲染:
- 列表项包含标题、寄件人、收件人、描述、状态5个文本区域
- 寄件人、收件人文本初始先加载Consignment对象里存储的用户uid
- 基于uid异步查询Firestore的
Users集合,拿到用户的姓名、职位、公司信息后,替换对应TextView的显示内容
问题现象
日志打印显示所有Firestore查询均执行成功、返回数据正确,但实际渲染时只有首个列表项(少数情况为首尾列表项)的寄件人、收件人文本能成功替换为完整用户信息,其余列表项始终停留在初始uid显示状态,滑动列表时还会出现文本错位、重复问题。
问题根因
- 把列表项的TextView引用、查询返回的User对象全定义成了Adapter的全局成员:ListView自带View复用逻辑,列表滚动时
getView会反复触发,全局存的TextView引用会不断被新绑定的列表项覆盖,等Firestore异步查询返回结果的时候,这个引用早就指到最后一次绑定的列表项上去了,之前发起查询的列表项根本拿不到更新。 - 写了
readData、readRec两个逻辑完全一模一样的查询方法,还把查询到的用户信息存在全局的con、rec变量里,多个列表项同时发查询请求的时候,这两个全局变量会被后返回的结果反复覆盖,回调里拿到的用户数据根本不是当前列表项对应的。 - 没做列表项位置校验:异步查询耗时没有准数,等结果回来的时候,原来发起查询的View说不定已经被复用来显示其他位置的内容了,直接setText百分百会出现内容错位。
修复方案
- 用标准ViewHolder模式缓存列表项里的控件,把ViewHolder存在convertView的tag里,彻底删掉Adapter类里全局的TextView引用,避免引用被覆盖。
- 删掉全局的
con、rec用户变量,查询到的结果直接通过回调参数传递,别存在全局,避免数据被覆盖。 - 把两个重复的查询方法合并成一个,减少冗余代码。
- 发起异步查询的时候,给当前View绑定对应的position标记,等回调触发的时候先校验标记,只有标记和当前position对得上的时候才更新文本,从根源上避免复用导致的错位。
- 加个简单的内存缓存,已经查过的用户信息直接从缓存取,不用每次都发Firestore请求,既省流量又能提升加载速度。
修复后完整代码
package com.example.consigness; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.TextView; import androidx.annotation.NonNull; import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.QueryDocumentSnapshot; import com.google.firebase.firestore.QuerySnapshot; import java.util.HashMap; import java.util.List; public class ConsignmentAdapter extends ArrayAdapter<Consignment> { // Firestore实例 private final FirebaseFirestore db = FirebaseFirestore.getInstance(); // 用户信息内存缓存,避免重复查询 private final HashMap<String, User> userCache = new HashMap<>(); // ViewHolder缓存列表项控件 private static class ViewHolder { TextView title_cList; TextView consignor_cList; TextView recipient_cList; TextView desc_cList; TextView status_cList; } public ConsignmentAdapter(Context context, List<Consignment> object){ super(context,0, object); } @Override public View getView(int position, View convertView, ViewGroup parent){ ViewHolder holder; if(convertView == null){ convertView = ((Activity)getContext()).getLayoutInflater().inflate(R.layout.consignment_list,parent,false); holder = new ViewHolder(); holder.title_cList = convertView.findViewById(R.id.cTitle_cList); holder.consignor_cList = convertView.findViewById(R.id.cCons_cList); holder.recipient_cList = convertView.findViewById(R.id.cRec_cList); holder.desc_cList = convertView.findViewById(R.id.cDesc_cList); holder.status_cList = convertView.findViewById(R.id.cStatus_cList); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } Consignment consignment = getItem(position); // 固定不变的文本直接设置 holder.title_cList.setText(consignment.getTitle()); holder.desc_cList.setText(consignment.getDesc()); holder.status_cList.setText("Status: "+consignment.getStatus()); // 处理寄件人信息 String consignorId = consignment.getConsignee(); holder.consignor_cList.setText("By: "+consignorId); // 绑定当前位置标记 convertView.setTag(R.id.tag_position_consignor, position); loadUserInfo(consignorId, user -> { // 校验位置标记,避免复用错位 Object tag = convertView.getTag(R.id.tag_position_consignor); if (tag instanceof Integer && (int)tag == position) { holder.consignor_cList.setText("By: "+user.getFname()+": "+user.getPost()+" at "+user.getCname()); } }); // 处理收件人信息 String recipientId = consignment.getRecipient(); holder.recipient_cList.setText("To: "+recipientId); // 绑定当前位置标记 convertView.setTag(R.id.tag_position_recipient, position); loadUserInfo(recipientId, user -> { // 校验位置标记,避免复用错位 Object tag = convertView.getTag(R.id.tag_position_recipient); if (tag instanceof Integer && (int)tag == position) { holder.recipient_cList.setText("To: "+user.getFname()+": "+user.getPost()+" at "+user.getCname()); } }); return convertView; } // 统一用户查询回调 private interface UserLoadCallback{ void onUserLoaded(User user); } // 合并重复的用户查询方法 private void loadUserInfo(String uid, UserLoadCallback callback){ // 先查缓存,有数据直接返回 if (userCache.containsKey(uid)) { callback.onUserLoaded(userCache.get(uid)); return; } db.collection("Users") .whereEqualTo("uid", uid) .get() .addOnCompleteListener(task -> { if (task.isSuccessful() && task.getResult() != null) { User user = null; for (QueryDocumentSnapshot document : task.getResult()) { Log.d("USER_LOAD", document.getId() + " => " + document.getData()); user = document.toObject(User.class); break; } if (user != null) { userCache.put(uid, user); callback.onUserLoaded(user); } } else { Log.d("USER_LOAD", "Error getting documents: ", task.getException()); } }); } }
注意:上面代码里用来存position的tag不要直接用控件id避免冲突,在
res/values/下新建ids.xml文件,定义两个专用id即可:
<?xml version="1.0" encoding="utf-8"?> <resources> <item name="tag_position_consignor" type="id" /> <item name="tag_position_recipient" type="id" /> </resources>
内容的提问来源于stack exchange,提问作者abbujaansboy
相关产品推荐
相关产品推荐

