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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 12:24:01