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

Flutter结合Cubit与Firebase API实现任务列表无限滚动

Flutter+Firebase 适配现有架构的无限滚动分页实现

第一步:修改后端Firebase分页查询逻辑

原有逻辑全量拉取数据,需要新增分页游标、单页条数限制,同时返回是否还有剩余数据的标识,方便前端判断是否需要继续加载。
基于你现有的查询代码修改如下:

async getTasksByFilter(
  filters: Array<IFilter>,
  pageSize: number = 20,
  lastCreatedAt?: number | Date
): Promise<{tasks: Array<ITask>, hasMore: boolean}> {
    let tasksUser: Array<ITask> = [];
    let collectionQuery: Query<DocumentData> = this.db.collection(this.taskCollection);
    let query = collectionQuery;
    return new Promise((resolve, reject) => {
        // 保留原有过滤逻辑
        filters.forEach(entry => {
            switch (entry.searchType) {
                case 'where':
                    query = query.where(
                        entry.field,
                        entry.condition as WhereFilterOp,
                        entry.value,
                    );
                    break;
                default:
                    break;
            }
        });
        // 新增排序+单页条数限制
        query = query.orderBy('createdAt', 'asc').limit(pageSize);
        // 非第一页时,以上一页最后一条数据的创建时间为查询起点
        if (lastCreatedAt) {
            query = query.startAfter(lastCreatedAt);
        }

        query.get()
            .then(querySnapshot => {
                querySnapshot.docs.forEach(doc => {
                    let task: ITask = this.transformDate(doc.data());
                    tasksUser.push(task);
                });
                resolve({
                    tasks: tasksUser,
                    // 返回条数等于单页限制时,说明可能还有更多数据
                    hasMore: tasksUser.length === pageSize
                });
            })
            .catch(error => {
                reject(error);
            });
    });
} 

提示:如果存在同一毫秒创建多条任务的场景,可将文档ID加入排序规则,游标传[lastCreatedAt, lastDocId],避免分页漏数。


第二步:修改Flutter端API调用方法

对应后端接口调整,新增分页参数传递,解析后端返回的分页结构:

// 新增分页结果封装类,可放在Task模型同目录下
class TaskPageResult {
  final List<Task> tasks;
  final bool hasMore;
  const TaskPageResult({required this.tasks, required this.hasMore});
}

Future<TaskPageResult> getUserTasks(
  String _extension, 
  Filter filters,
  {int pageSize = 20, DateTime? lastCreatedAt}
) async {
    // 将分页参数加入请求体
    final requestBody = filters.toJson();
    requestBody['pageSize'] = pageSize;
    if (lastCreatedAt != null) {
      requestBody['lastCreatedAt'] = lastCreatedAt.toUtc().millisecondsSinceEpoch;
    }

    final Response response = await client.post(
      Uri.parse("$BASE_URL$_extension"),
      headers: <String, String>{
        'Content-Type': 'application/json; charset=UTF-8',
      },
      body: jsonEncode(requestBody),
    );

    final responseData = jsonDecode(response.body);
    List<Task> tasks = List<Task>.from(
      (responseData['tasks'] as List).map((model) => Task.fromJson(model))
    );

    return TaskPageResult(
      tasks: tasks,
      hasMore: responseData['hasMore'] as bool
    );
  }

后端接口层需要从请求体中读取pageSize和lastCreatedAt参数,传入修改后的查询方法即可。


第三步:调整Cubit状态与业务逻辑

在原有状态基础上新增分页相关字段,新增加载下一页的方法,不破坏原有初始化加载逻辑:

  1. 修改LoadedTaskListState,新增三个字段:
    • hasMore:是否还有未加载的数据
    • isLoadingMore:是否正在加载下一页,控制底部加载指示器
    • lastCreatedAt:当前列表最后一条任务的创建时间,作为下一次查询的游标
  2. 在Cubit中新增加载下一页的方法:
Future<void> loadMoreTasks() async {
  final currentState = state;
  if (currentState is! LoadedTaskListState) return;
  // 正在加载中或没有更多数据时直接返回,避免重复请求
  if (currentState.isLoadingMore || !currentState.hasMore) return;

  emit(currentState.copyWith(isLoadingMore: true));

  try {
    final result = await getUserTasks(
      '/your_task_api_path', // 替换为实际接口路径
      currentActiveFilters, // 全局存储当前生效的筛选条件
      lastCreatedAt: currentState.lastCreatedAt,
    );

    final allTasks = [...currentState.tasks, ...result.tasks];
    emit(LoadedTaskListState(
      tasks: allTasks,
      hasMore: result.hasMore,
      isLoadingMore: false,
      lastCreatedAt: allTasks.isNotEmpty ? allTasks.last.createdAt : null,
      // 原有tab相关参数保持不变
      tabIndex: currentState.tabIndex,
      tabName: currentState.tabName,
      tabType: currentState.tabType,
    ));
  } catch (e) {
    // 加载失败时重置加载状态,可按需添加轻量提示
    emit(currentState.copyWith(isLoadingMore: false));
  }
}

原有初始化加载第一页的逻辑同步调整:拉取完成后设置lastCreatedAt为第一页最后一条数据的创建时间,hasMore取接口返回值,isLoadingMore默认false即可。切换Tab、修改筛选条件时需要重置所有分页参数,重新拉取第一页数据,避免不同筛选条件下的数据混淆。


第四步:修改UI层,加入滚动监听与加载状态

给列表添加滚动控制器,监听滚动位置触发加载,同时在列表底部渲染加载状态:

  1. 在列表所在的StatefulWidget中声明并释放滚动控制器:
final ScrollController _scrollController = ScrollController();

@override
void initState() {
  super.initState();
  _scrollController.addListener(() {
    // 滚动到距离底部200像素位置时触发加载更多
    if (_scrollController.position.pixels >= 
        _scrollController.position.maxScrollExtent - 200) {
      context.read<TaskListCubit>().loadMoreTasks();
    }
  });
}

@override
void dispose() {
  _scrollController.dispose();
  super.dispose();
}
  1. 修改原有ListView.builder逻辑,绑定控制器、调整item计数、渲染底部加载状态:
Expanded(
  child: ListView.builder(
    controller: _scrollController,
    itemBuilder: (context, index) {
      // 最后一个位置渲染加载状态
      if (index == tasks.length) {
        if (state.isLoadingMore) {
          return const Padding(
            padding: EdgeInsets.symmetric(vertical: 16),
            child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
          );
        }
        return const SizedBox.shrink();
      }
      final task = tasks[index];
      return TaskCard(
        task,
        state.tabName,
        state.tabType,
        cubit,
        onClick: () {
          push(
            context,
            TaskDetailsContainer(task, state.tabType),
          );
        },
      );
    },
    // 有更多数据时多预留1个位置给加载指示器
    itemCount: tasks.length + (state.hasMore ? 1 : 0),
  ),
)

内容的提问来源于stack exchange,提问作者Gustavo F.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 18:45:39