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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 08:01:11