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

Flutter Firestore获取8000条文档数据ListView不显示问题排查

解决Firestore获取8000条数据后ListView无显示的问题

问题描述

尝试从Firestore中获取约8000条文档数据,存入列表后用ListView展示,但页面无任何显示。该方法在获取50条左右的小数据集合时可正常运行,不确定是否因数据量过大导致问题。

原始代码

class FoodDetailsModel{
  late String name; late String caffeine; late String calcium;

  FoodDetailsModel({
    required this.name, required this.caffeine, required this.calcium,
  });

  FoodDetailsModel.fromJson(dynamic json)
  {
    name=json['name'];name=json['caffeine'];name=json['calcium'];
  }
  dynamic toMap()
  {
    return {
      'name':name, 'caffeine':caffeine, 'calcium':calcium,
    };
  }
}

void getFoodDetails()
{
  FirebaseFirestore.instance
      .collection('nutrition')
      .get()
      .then((value){
        foodDetailsList = [];
        value.docs.forEach((element)
        {
          foodDetailsList.add(FoodDetailsModel.fromJson(element.data()));
        });
        emit(getFoodDetailsSuccussState());
  });
}

class FoodDetailsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocConsumer<NutritionCubit,LifeStates>(
      listener: (context,state){},
      builder: (context,state)
      {
        NutritionCubit.get(context).getFoodDetails();
        return  Scaffold(
          appBar: AppBar(
            title: Text('Diets'),
          ),
          body: Column(
            children: [
              ListView.separated(
                  itemBuilder: (context,index) => buildFoodDetails(context,NutritionCubit.get(context).foodDetailsList[index]),
                  itemCount: NutritionCubit.get(context).foodDetailsList.length,
                separatorBuilder: (BuildContext context, int index) => SizedBox(height: 20,),
              ),
            ],
          )
        );
      },
    );
  }
}

Widget buildFoodDetails(context,FoodDetailsModel model)
{
  return Text(model.name);
}

问题分析与解决方案

1. 修复Model类的fromJson方法错误

原始代码中fromJson方法把三个字段都赋值给了name,导致caffeine和calcium从未被正确初始化,甚至可能因字段缺失引发异常。

修改后的Model类:

class FoodDetailsModel{
  late String name; 
  late String caffeine; 
  late String calcium;
  // 分页需要:保存文档快照用于下一页查询
  late DocumentSnapshot documentSnapshot;

  FoodDetailsModel({
    required this.name, 
    required this.caffeine, 
    required this.calcium,
    required this.documentSnapshot,
  });

  FoodDetailsModel.fromJson(dynamic json, DocumentSnapshot snapshot)
  {
    // 用空字符串兜底,避免字段不存在时崩溃
    name = json['name'] ?? '';
    caffeine = json['caffeine'] ?? '';
    calcium = json['calcium'] ?? '';
    documentSnapshot = snapshot;
  }

  dynamic toMap()
  {
    return {
      'name':name, 
      'caffeine':caffeine, 
      'calcium':calcium,
    };
  }
}

2. 解决ListView在Column中的高度约束问题

Column是无限高度容器,ListView默认也会占用无限高度,导致无法渲染。用Expanded包裹ListView,让它占据Column的剩余空间:

body: Column(
  children: [
    Expanded(
      child: ListView.separated(
        itemBuilder: (context,index) => buildFoodDetails(context,NutritionCubit.get(context).foodDetailsList[index]),
        itemCount: NutritionCubit.get(context).foodDetailsList.length,
        separatorBuilder: (BuildContext context, int index) => SizedBox(height: 20,),
      ),
    ),
  ],
)

3. 避免重复触发数据请求

原始代码在build方法中每次都会调用getFoodDetails(),导致重复请求Firestore,浪费资源且引发状态混乱。改为StatefulWidget,在initState中仅调用一次:

class FoodDetailsScreen extends StatefulWidget {
  @override
  State<FoodDetailsScreen> createState() => _FoodDetailsScreenState();
}

class _FoodDetailsScreenState extends State<FoodDetailsScreen> {
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    NutritionCubit.get(context).getFoodDetails();
    // 监听滑动到底部,触发分页加载
    _scrollController.addListener(() {
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent) {
        NutritionCubit.get(context).getFoodDetails();
      }
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return BlocConsumer<NutritionCubit,LifeStates>(
      listener: (context,state){
        // 添加错误提示
        if (state is GetFoodDetailsErrorState) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(state.error)),
          );
        }
      },
      builder: (context,state)
      {
        // 显示加载状态
        if (state is GetFoodDetailsLoadingState) {
          return const Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }
        
        return Scaffold(
          appBar: AppBar(title: const Text('Diets')),
          body: Column(
            children: [
              Expanded(
                child: ListView.separated(
                  controller: _scrollController,
                  itemBuilder: (context,index) => buildFoodDetails(context,NutritionCubit.get(context).foodDetailsList[index]),
                  itemCount: NutritionCubit.get(context).foodDetailsList.length,
                  separatorBuilder: (_,__) => const SizedBox(height: 20),
                ),
              ),
            ],
          )
        );
      },
    );
  }
}

4. 优化大数据量加载:分页获取数据

一次性加载8000条数据会导致内存占用过高、加载缓慢甚至请求超时,必须用分页加载优化:

修改Cubit中的数据获取方法,支持分页:

class NutritionCubit extends Cubit<LifeStates> {
  NutritionCubit() : super(InitialState());

  static NutritionCubit get(context) => BlocProvider.of(context);

  List<FoodDetailsModel> foodDetailsList = [];
  int _currentPage = 0;
  final int _pageSize = 50; // 每页加载50条
  bool _hasMore = true;

  void getFoodDetails({bool isRefresh = false}) {
    if (!_hasMore && !isRefresh) return;
    emit(GetFoodDetailsLoadingState());

    Query query = FirebaseFirestore.instance
        .collection('nutrition')
        .limit(_pageSize);

    // 非刷新状态下,基于最后一条文档加载下一页
    if (!isRefresh && foodDetailsList.isNotEmpty) {
      query = query.startAfterDocument(foodDetailsList.last.documentSnapshot);
    } else {
      // 刷新时重置分页状态
      _currentPage = 0;
      _hasMore = true;
      foodDetailsList.clear();
    }

    query.get().then((value) {
      if (value.docs.length < _pageSize) {
        _hasMore = false;
      }
      for (var element in value.docs) {
        foodDetailsList.add(FoodDetailsModel.fromJson(element.data(), element));
      }
      emit(GetFoodDetailsSuccessState());
    }).catchError((error) {
      emit(GetFoodDetailsErrorState(error.toString()));
    });
  }
}

5. 补充状态类定义

确保Cubit对应的状态类已定义:

abstract class LifeStates {}

class InitialState extends LifeStates {}

class GetFoodDetailsLoadingState extends LifeStates {}

class GetFoodDetailsSuccessState extends LifeStates {}

class GetFoodDetailsErrorState extends LifeStates {
  final String error;
  GetFoodDetailsErrorState(this.error);
}

内容的提问来源于stack exchange,提问作者hazem saad

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 20:00:03