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

Flutter Riverpod分页:如何正确调用init()避免构建报错?

问题

我尝试用Riverpod结合Firestore实现列表分页功能,视图页面和Provider代码如下,但在调用init()函数时出现错误:Unhandled Exception: Tried to modify a provider while the widget tree was building.,请问如何正确实现分页?

视图页面代码

import 'package:enue/community/gyeongin/addposting/add_posting.dart';
import 'package:enue/provider/posting/posting_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class PostingHome extends ConsumerStatefulWidget {
  const PostingHome({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _PostingHomeState();
}

class _PostingHomeState extends ConsumerState<PostingHome> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) {
                return const AddPosting();
              },
            ),
          );
        },
        backgroundColor: const Color(0xFFc54245),
        child: const Icon(Icons.create, color: Colors.white),
      ),
      body: Consumer(
        builder: (context, ref, widget) {
          final postingHomeState = ref.watch(postingHomeProvider);

          return ListView.builder(
            controller: postingHomeState.scrollController,
            itemCount: postingHomeState.list.length + 1,
            itemBuilder: (context, index) {
              if (index < postingHomeState.list.length) {
                final item = postingHomeState.list[index];
                return ListTile(
                  title: Text(item['title']),
                  subtitle: Text(item['subtitle']),
                );
              } else if (postingHomeState.isMore) {
                return const Center(child: CircularProgressIndicator());
              } else {
                return Container();
              }
            },
          );
        },
      ),
    );
  }
}

Provider代码

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

final postingHomeProvider = StateNotifierProvider.autoDispose<
    PostingHomeStateNotifier,
    PostingHomeState>((ref) => PostingHomeStateNotifier());

class PostingHomeState {
  const PostingHomeState({
    this.isMore = true,
    this.isLoading = false,
    this.list = const [],
    required this.scrollController,
  });

  final bool isMore;
  final bool isLoading;
  final List<Map<String, dynamic>> list;
  final ScrollController scrollController;

  PostingHomeState copyWith({
    bool? isMore,
    bool? isLoading,
    List<Map<String, dynamic>>? list,
    ScrollController? scrollController,
  }) {
    return PostingHomeState(
      isMore: isMore ?? this.isMore,
      isLoading: isLoading ?? this.isLoading,
      list: list ?? this.list,
      scrollController: scrollController ?? this.scrollController,
     );
  }
}

class PostingHomeStateNotifier extends StateNotifier<PostingHomeState> {
  PostingHomeStateNotifier()
      : super(PostingHomeState(
            scrollController: ScrollController(),
            list: const [],
            isLoading: false,
            isMore: true));

  DocumentSnapshot? lastDocument;

  Future<void> paginatedData() async {
    if (state.isMore) {
      state = state.copyWith(isLoading: true);
      final collectionReference = FirebaseFirestore.instance
          .collection("경인교대(게시글)")
          .orderBy("createdAt", descending: true);

      late QuerySnapshot<Map<String, dynamic>> querySnapshot;

      if (lastDocument == null) {
        querySnapshot = await collectionReference.limit(10).get();
      } else {
        querySnapshot = await collectionReference
            .limit(10)
            .startAfterDocument(lastDocument!)
            .get();
      }

      lastDocument =
          querySnapshot.docs.isEmpty ? null : querySnapshot.docs.last;

      state = state.copyWith(
          list: [...state.list, ...querySnapshot.docs.map((e) => e.data())],
          isLoading: false);

      if (querySnapshot.docs.length < 10) {
        state = state.copyWith(isMore: false);
      }
    }
  }

  void onScroll() {
    if (state.scrollController.position.pixels ==
        state.scrollController.position.maxScrollExtent) {
      paginatedData();
    }
  }

  void init() {
    paginatedData();
    state.scrollController.addListener(onScroll);
  }

  @override
  void dispose() {
    state.scrollController.dispose();
    super.dispose();
  }
}

解决方案

1. 错误原因

你在Widget的构建阶段(build方法里)调用了init(),而这个方法会触发Provider状态修改(比如paginatedData里更新list和isLoading),Flutter禁止在构建过程中修改状态,否则会导致渲染异常。

2. 正确初始化方式

方式一:在initState中调用初始化

把init()放到ConsumerState的initState方法里,这里是Widget初始化的安全阶段,不会触发构建时的状态修改错误:

class _PostingHomeState extends ConsumerState<PostingHome> {
  @override
  void initState() {
    super.initState();
    // 获取notifier并调用初始化
    ref.read(postingHomeProvider.notifier).init();
  }

  @override
  Widget build(BuildContext context) {
    // 原build代码保持不变
    return Scaffold(
      // ... 省略原有代码
    );
  }
}

方式二:Provider内部自动初始化

如果不想在视图层手动调用init(),可以把初始化逻辑放到StateNotifier的构造函数里,用Future.microtask延迟执行,避免同步修改状态:

class PostingHomeStateNotifier extends StateNotifier<PostingHomeState> {
  PostingHomeStateNotifier()
      : super(PostingHomeState(
            scrollController: ScrollController(),
            list: const [],
            isLoading: false,
            isMore: true)) {
    // 用微任务延迟执行,避免初始化时同步修改状态
    Future.microtask(() {
      paginatedData();
      state.scrollController.addListener(onScroll);
    });
  }

  // 省略其他已有方法...
}

这样Provider创建时会自动完成初始化,不需要在视图层额外调用。

3. 分页逻辑优化建议

  • 防止重复请求:在paginatedData开头加判断,避免加载中或无更多数据时重复触发请求:
Future<void> paginatedData() async {
  // 新增判断:加载中或无更多数据时直接返回
  if (state.isLoading || !state.isMore) return;
  
  state = state.copyWith(isLoading: true);
  // 省略原有请求逻辑...
}
  • 滚动监听优化:可以用NotificationListener替代ScrollController的监听,或者在onScroll里加防抖处理,避免快速滚动时多次触发请求。

内容的提问来源于stack exchange,提问作者박진혁

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 09:27:08