setState() or markNeedsBuild() called during build.が発生した場合の原因と対策

技術

StatefulWidgetとProviderを併用していたところ、以下のエラーに遭遇しました。

The following assertion was thrown while dispatching notifications for TestState:
setState() or markNeedsBuild() called during build.

This _InheritedProviderScope<TestState?> 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: _InheritedProviderScope<TestState?>
  value: Instance of 'TestState'
  listening to value
The widget which was currently being built when the offending call was made was: MyHomePage
  dirty
  dependencies: [_InheritedProviderScope<TestState?>, _InheritedTheme, _LocalizationsScope-[GlobalKey#cd673]]
  state: _MyHomePageState#14150

以下のmain.dartで発生しました。

main.dart

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
        create: (context) => TestState(),
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
            useMaterial3: true,
          ),
          home: const MyHomePage(title: 'Flutter Demo Home Page'),
        ));
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _incrementCounterState(TestState testState) {
    testState.incrementTestCount();
  }

  @override
  Widget build(BuildContext context) {
    var testState = context.watch<TestState>();
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
            const SizedBox(height: 60),
            Text(
              '${testState.getTestCount()}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            FloatingActionButton(
              onPressed: () {
                _incrementCounterState(testState);
              },
              tooltip: 'Increment2',
              child: const Icon(Icons.add),
              shape: LinearBorder.end(),
            ),
          ],
        ),
      ),
    );
  }
}

ビルドしたら以下のように2つのボタンが表示されて、ボタンを押したらそれぞれ数字がプラス1されます(デフォルトのカウンターアプリに少し機能追加したものです)。

上の丸型のボタンはsetStateボタンで更新され、下の四角ボタンはProviderのnotifyListener()で更新されます。

ここで、例えば以下のようにmain.dartのbuildメソッドの前に下の四角ボタンの更新処理を走らせた場合に上記のエラーが出ました。

main.dart

  ~~~省略~~~
  @override
  Widget build(BuildContext context) {
    var testState = context.watch<TestState>();
    _incrementCounterState(testState); // 追加
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
            const SizedBox(height: 60),
            Text(
              '${testState.getTestCount()}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            FloatingActionButton(
              onPressed: () {
                _incrementCounterState(testState);
              },
              tooltip: 'Increment2',
              child: const Icon(Icons.add),
              shape: LinearBorder.end(),
            ),
          ],
        ),
      ),
    );
  }
  ~~~省略~~~

エラーメッセージを読んでみた感じだと、ビルド中にsetStateやmarkNeedsBuild()(おそらくnotifyListenerを呼び出した際に呼ばれるメソッド)を呼び出すと発生するようですね。

ただ、この事象が起きるのはnotifyListenerのみのようで、以下のようにsetState()を呼ぶメソッドに変えた場合は起きなかったです。なぜだ。。。

 ~~~省略~~~
  @override
  Widget build(BuildContext context) {
    var testState = context.watch<TestState>();
    // _incrementCounterState(testState); // 追加
    _incrementCounter // 追加
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            FloatingActionButton(
              onPressed: _incrementCounter,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
            const SizedBox(height: 60),
            Text(
              '${testState.getTestCount()}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            FloatingActionButton(
              onPressed: () {
                _incrementCounterState(testState);
              },
              tooltip: 'Increment2',
              child: const Icon(Icons.add),
              shape: LinearBorder.end(),
            ),
          ],
        ),
      ),
    );
  }
  ~~~省略~~~

対策

いずれにせよ、このエラーを出さないためにはbuildメソッドを呼ぶ前にsetStateやnotifyListener()を呼ばないように気をつける必要がありそうです。buildメソッド前に状態を更新したい!みたいな機会は割と多い気がするので、気をつけていきたいですね。

コメント