如何使用Riverpod ChangeNotifier实现Firestore分页(无限滚动/懒加载)
基于Riverpod ChangeNotifier实现Firestore分页方案
前置依赖
首先在pubspec.yaml中引入必要依赖:
dependencies: flutter: sdk: flutter flutter_riverpod: ^2.3.0 cloud_firestore: ^4.8.0
步骤1:实现分页逻辑的ChangeNotifier
该类核心承载数据存储、分页状态管理、两类拉取逻辑的实现:
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/foundation.dart'; // 自定义模型示例,可替换为你自己的业务模型 class Post { final String id; final String title; Post.fromFirestore(DocumentSnapshot doc) : id = doc.id, title = doc.get('title') as String; } class PostPaginationNotifier extends ChangeNotifier { // 分页配置 final int pageSize = 10; // 数据存储 List<Post> items = []; // 状态管理 bool isLoadingFirstPage = false; bool isLoadingNextPage = false; bool hasMore = true; // 分页游标 DocumentSnapshot? lastDocument; // 拉取首批数据 Future<void> fetchFirstPage() async { isLoadingFirstPage = true; hasMore = true; lastDocument = null; items.clear(); notifyListeners(); try { final query = FirebaseFirestore.instance .collection('posts') .orderBy('createTime', descending: true) .limit(pageSize); final snapshot = await query.get(); if (snapshot.docs.isEmpty) { hasMore = false; isLoadingFirstPage = false; notifyListeners(); return; } lastDocument = snapshot.docs.last; items = snapshot.docs.map((doc) => Post.fromFirestore(doc)).toList(); hasMore = snapshot.docs.length == pageSize; } catch (e) { // 可自定义异常处理逻辑,比如存储错误信息对外暴露 if (kDebugMode) print('拉取首页失败: $e'); } finally { isLoadingFirstPage = false; notifyListeners(); } } // 拉取下一批数据 Future<void> fetchNextPage() async { // 避免重复请求 if (isLoadingNextPage || !hasMore || lastDocument == null) return; isLoadingNextPage = true; notifyListeners(); try { final query = FirebaseFirestore.instance .collection('posts') .orderBy('createTime', descending: true) .startAfterDocument(lastDocument!) .limit(pageSize); final snapshot = await query.get(); if (snapshot.docs.isEmpty) { hasMore = false; isLoadingNextPage = false; notifyListeners(); return; } lastDocument = snapshot.docs.last; // 新数据追加到已有列表 items.addAll(snapshot.docs.map((doc) => Post.fromFirestore(doc)).toList()); hasMore = snapshot.docs.length == pageSize; } catch (e) { if (kDebugMode) print('拉取下页失败: $e'); } finally { isLoadingNextPage = false; notifyListeners(); } } }
如果需要复用分页逻辑,可以将该类改为泛型实现,传入集合路径、排序规则、模型转换函数即可适配所有业务场景。
步骤2:声明ChangeNotifierProvider
import 'package:flutter_riverpod/flutter_riverpod.dart'; final postPaginationProvider = ChangeNotifierProvider.autoDispose<PostPaginationNotifier>((ref) { final notifier = PostPaginationNotifier(); // 初始化时自动拉取第一页 notifier.fetchFirstPage(); return notifier; });
如果需要内存缓存能力,去掉autoDispose修饰即可,页面销毁后已拉取的数据不会被释放,下次进入页面直接复用已有数据。
步骤3:Widget层集成滚动监听与视图渲染
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class PostListScreen extends ConsumerStatefulWidget { const PostListScreen({super.key}); @override ConsumerState<PostListScreen> createState() => _PostListScreenState(); } class _PostListScreenState extends ConsumerState<PostListScreen> { final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); // 监听滚动事件,接近底部时触发拉取下一页 _scrollController.addListener(_onScroll); } void _onScroll() { final position = _scrollController.position; // 距离底部还有200px时提前触发加载,可根据业务调整阈值 if (position.pixels >= position.maxScrollExtent - 200 && !position.outOfRange) { ref.read(postPaginationProvider.notifier).fetchNextPage(); } } @override void dispose() { _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final paginationState = ref.watch(postPaginationProvider); return Scaffold( appBar: AppBar(title: const Text('分页列表')), body: paginationState.isLoadingFirstPage ? const Center(child: CircularProgressIndicator()) : paginationState.items.isEmpty ? const Center(child: Text('暂无数据')) : ListView.builder( controller: _scrollController, itemCount: paginationState.items.length + (paginationState.isLoadingNextPage ? 1 : 0), itemBuilder: (context, index) { // 渲染底部加载更多指示器 if (index == paginationState.items.length) { return const Padding( padding: EdgeInsets.all(16), child: Center(child: CircularProgressIndicator()), ); } // 渲染业务项 final post = paginationState.items[index]; return ListTile(title: Text(post.title)); }, ), ); } }
额外能力适配
- 自定义模型映射:直接在数据转换层调用你的模型
fromFirestore方法即可,完全可控无限制 - 离线缓存:Firestore默认开启离线持久化,已拉取过的数据在离线状态下也可以正常访问,无需额外开发
内容的提问来源于stack exchange,提问作者Theo
相关产品推荐
相关产品推荐

