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状态与业务逻辑
在原有状态基础上新增分页相关字段,新增加载下一页的方法,不破坏原有初始化加载逻辑:
- 修改
LoadedTaskListState,新增三个字段:hasMore:是否还有未加载的数据isLoadingMore:是否正在加载下一页,控制底部加载指示器lastCreatedAt:当前列表最后一条任务的创建时间,作为下一次查询的游标
- 在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层,加入滚动监听与加载状态
给列表添加滚动控制器,监听滚动位置触发加载,同时在列表底部渲染加载状态:
- 在列表所在的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(); }
- 修改原有
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.
相关产品推荐
相关产品推荐

