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

Flutter:清空ListView项时ChangeNotifierProvider崩溃——构建期间调用setState

自定义下拉刷新指示器触发setState崩溃问题解决

问题描述

我正在为ListView实现自定义下拉刷新指示器,通过ChangeNotifier结合滚动控制器更新offset变量,自定义刷新指示器借助Consumer组件更新UI。当检测到滚动偏移量小于-100时,会调用onRefresh函数。预期onRefresh逻辑是先清空现有列表项,再获取新数据展示,但执行清空操作时出现崩溃;移除清空代码则功能正常。

报错信息

Exception has occurred.
FlutterError (setState() or markNeedsBuild() called during build.
This ViewController widget cannot be marked as needing to build because the framework is already in the process of building widgets. A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was:
  ViewController
The widget which was currently being built when the offending call was made was:
  Consumer<ScrollProvider>)

相关代码

viewcontroller.dart

import 'dart:math';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:notifydemo/refreshcontrol.dart';
import 'package:provider/provider.dart';

class ViewController extends StatefulWidget {
  const ViewController({super.key});

  @override
  State<ViewController> createState() => _ViewControllerState();
}

class _ViewControllerState extends State<ViewController> {
  final scrollController = ScrollController();
  final scrollProvider = ScrollProvider();
  final refreshIndicatorKey = GlobalKey<RefreshControlState>();
  List<String> items = [];

  @override
  void initState() {
    scrollController.addListener(scrollHandler);

    WidgetsBinding.instance.addPostFrameCallback((_) {
      refreshIndicatorKey.currentState?.show();
    });
    super.initState();
  }

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

  void scrollHandler() {
    scrollProvider.setCurrentOffset(scrollController.offset);
  }

  @override
  Widget build(BuildContext context) {
    print("Build");
    return ChangeNotifierProvider(
      create: ((_) => scrollProvider),
      child: RefreshControl(
        key: refreshIndicatorKey,
        onRefresh: fetchData,
        child: ListView.builder(
          controller: scrollController,
          itemBuilder: ((context, index) {
            return Text(
              "$index. ${items[index]}",
              style: const TextStyle(fontSize: 40),
            );
          }),
          itemCount: items.length,
        ),
      ),
    );
  }

  Future fetchData() async {
    setState(() {
      items.clear();
    });
    final returned = await slowNumbers();
    setState(() {
      items.addAll(returned);
    });
  }
}

Future<List<String>> slowNumbers() async {
  return Future.delayed(
    const Duration(milliseconds: 2000),
    () => List.generate(50, (index) => Random().nextInt(99999).toString()),
  );
}

class ScrollProvider extends ChangeNotifier {
  double? currentOffset;
  void setCurrentOffset(valueToSet) {
    if (currentOffset != valueToSet) {
      currentOffset = valueToSet;
      print("currentOffset: $currentOffset");
      notifyListeners();
    }
  }
}

refreshcontrol.dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'viewcontroller.dart';

class RefreshControl extends StatefulWidget {
  const RefreshControl({super.key, required this.onRefresh, required this.child});
  final Widget child;
  final Future<void> Function() onRefresh;
  @override
  State<RefreshControl> createState() => RefreshControlState();
}

class RefreshControlState extends State<RefreshControl> {
  bool isRefreshing = false;

  void show() {
    widget.onRefresh();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        Positioned(
          left: 0,
          top: 50,
          right: 0,
          child: CupertinoButton(
            onPressed: show,
            child: Consumer<ScrollProvider>(
              builder: ((context, provider, child) {
                if (!isRefreshing && (provider.currentOffset ?? 0) < -100) {
                  isRefreshing = true;
                  show();
                }
                return Text(
                  "Offset: ${provider.currentOffset?.toInt()}",
                  style: const TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                    backgroundColor: Colors.black,
                  ),
                );
              }),
            ),
          ),
        ),
      ],
    );
  }
}

问题原因

崩溃核心原因:在Consumer的builder方法(属于Widget构建阶段)中直接调用show(),进而触发onRefresh里的setState。Flutter禁止在构建过程中调用setState/markNeedsBuild,此时框架正在处理Widget树构建,状态更新会与构建流程冲突。同时,isRefreshing的状态修改直接放在builder方法中,违反了构建方法不能有副作用的原则。

解决方法

1. 重构RefreshControl逻辑

将偏移量检测和刷新触发逻辑从builder中移出,通过监听Provider变化+延迟执行的方式处理:

class RefreshControlState extends State<RefreshControl> {
  bool isRefreshing = false;
  ScrollProvider? _scrollProvider;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final provider = Provider.of<ScrollProvider>(context);
    if (_scrollProvider != provider) {
      _scrollProvider?.removeListener(_checkOffset);
      _scrollProvider = provider;
      _scrollProvider?.addListener(_checkOffset);
    }
  }

  @override
  void dispose() {
    _scrollProvider?.removeListener(_checkOffset);
    super.dispose();
  }

  void _checkOffset() {
    if (!isRefreshing && (_scrollProvider?.currentOffset ?? 0) < -100) {
      isRefreshing = true;
      // 延迟到当前构建周期结束后执行刷新
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _performRefresh();
      });
    }
  }

  Future<void> _performRefresh() async {
    await widget.onRefresh();
    setState(() {
      isRefreshing = false;
    });
  }

  void show() {
    _performRefresh();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        widget.child,
        Positioned(
          left: 0,
          top: 50,
          right: 0,
          child: CupertinoButton(
            onPressed: show,
            child: Consumer<ScrollProvider>(
              builder: ((context, provider, child) {
                return Text(
                  "Offset: ${provider.currentOffset?.toInt()}",
                  style: const TextStyle(
                    fontSize: 32,
                    fontWeight: FontWeight.bold,
                    backgroundColor: Colors.black,
                  ),
                );
              }),
            ),
          ),
        ),
      ],
    );
  }
}

2. 优化fetchData方法(可选)

合并两次setState,减少不必要的UI刷新:

Future fetchData() async {
  final returned = await slowNumbers();
  setState(() {
    items.clear();
    items.addAll(returned);
  });
}

关键说明

  • 禁止在Widget的build方法(包括Consumer的builder)中执行有副作用的操作(修改状态、触发异步操作等),build方法应是纯函数,仅根据当前状态返回Widget树。
  • 使用WidgetsBinding.instance.addPostFrameCallback可确保代码在当前帧构建完成后执行,避免构建周期内的状态更新冲突。
  • 监听Provider变化时,优先使用didChangeDependencies或addListener,而非在build中处理业务逻辑。

内容的提问来源于stack exchange,提问作者sudoExclamationExclamation

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 00:01:14