PR

[Flutter]initStateで画面遷移する方法

Flutter

はじめに

StatefulWidgetのinitStateでいきなり画面遷移する際に、少しはまったのでメモです。

環境

  • Flutter 2.0.5 Null-Safety

問題点

initStateで画面遷移する際に最初は以下のようなコードを書いていました。

class _SecondPageState extends State<SecondPage> {
  @override
  void initState() {
    super.initState();

    Navigator.push(context, MaterialPageRoute(builder: (context) {
      return ThirdPage();
    }));
  }

すると以下のようなエラーを吐きます。

Performing hot reload...
Syncing files to device iPhone 12 Pro Max...
Reloaded 1 of 548 libraries in 411ms.

======== Exception caught by widgets library =======================================================
The following assertion was thrown building Builder:
setState() or markNeedsBuild() called during build.

This Overlay 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: Overlay-[LabeledGlobalKey<OverlayState>#26329]
  state: OverlayState#7bc52(entries: [OverlayEntry#a6e08(opaque: true; maintainState: false), OverlayEntry#40e7d(opaque: false; maintainState: true), OverlayEntry#0b74a(opaque: false; maintainState: false), OverlayEntry#5ede3(opaque: false; maintainState: true), OverlayEntry#8b7ef(opaque: false; maintainState: false), OverlayEntry#d01db(opaque: false; maintainState: true)])
The widget which was currently being built when the offending call was made was: Builder
The relevant error-causing widget was: 
  MaterialApp file:///~/Projects/Flutter/flutter_app/lib/main.dart:10:12
When the exception was thrown, this was the stack: 
#0      Element.markNeedsBuild.<anonymous closure> (package:flutter/src/widgets/framework.dart:4138:11)
#1      Element.markNeedsBuild (package:flutter/src/widgets/framework.dart:4153:6)
#2      State.setState (package:flutter/src/widgets/framework.dart:1287:15)
#3      OverlayState.rearrange (package:flutter/src/widgets/overlay.dart:436:5)
#4      NavigatorState._flushHistoryUpdates (package:flutter/src/widgets/navigator.dart:4043:16)
...
====================================================================================================
[VERBOSE-2:ui_dart_state.cc(186)] Unhandled Exception: 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 2997 pos 18: '!navigator._debugLocked': is not true.
#0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:46:39)
#1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:36:5)
#2      _RouteEntry.handlePush.<anonymous closure> (package:flutter/src/widgets/navigator.dart:2997:18)
#3      TickerFuture.whenCompleteOrCancel.thunk (package:flutter/src/scheduler/ticker.dart:407:15)
#4      _rootRunUnary (dart:async/zone.dart:1362:47)
#5      _CustomZone.runUnary (dart:async/zone.dart:1265:19)
#6      _FutureListener.handleValue (dart:async/future_impl.dart:152:18)
#7      Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:704:45)
#8      Future._propagateToListeners (dart:async/future_impl.dart:733:32)
#9      Future._completeWithValue (dart:async/future_impl.dart:539:5)
#10     Future._asyncCompleteWithValue.<anonymous closure><…>
[VERBOSE-2:ui_dart_state.cc(186)] Unhandled Exception: 'package:flutter/src/widgets/navigator.dart': Failed assertion: line 2997 pos 18: '!navigator._debugLocked': is not true.
#0      _AssertionError._doThrowNew (dart:core-patch/errors_patch.dart:46:39)
#1      _AssertionError._throwNew (dart:core-patch/errors_patch.dart:36:5)
#2      _RouteEntry.handlePush.<anonymous closure> (package:flutter/src/widgets/navigator.dart:2997:18)
#3      TickerFuture.whenCompleteOrCancel.thunk (package:flutter/src/scheduler/ticker.dart:407:15)
#4      _rootRunUnary (dart:async/zone.dart:1362:47)
#5      _CustomZone.runUnary (dart:async/zone.dart:1265:19)
#6      _FutureListener.handleValue (dart:async/future_impl.dart:152:18)
#7      Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:704:45)
#8      Future._propagateToListeners (dart:async/future_impl.dart:733:32)
#9      Future._completeWithValue (dart:async/future_impl.dart:539:5)
#10     Future._asyncCompleteWithValue.<anonymous closure><…>

これはWidgetのビルド中に状態を変更する際に起きるExceptionです。

つまり、initState内で状態を変更するような処理は書いてはいけないということです。

対策方法

ではどのように対策すればよいのかと言うと、Futureでくくればいいだけです。

class _SecondPageState extends State<SecondPage> {
  @override
  void initState() {
    super.initState();
    Future(() {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return ThirdPage();
      }));
    });
  }

なぜ、Futureでくくればいいのかと言うと。

Dartはシングルスレッドのイベントループに基づいて作らているため、非同期のタスクを作成すると、イベントキューの最後に配置されます。

現在の実行されているイベントが処理されたあとに、追加したタスクのイベントが処理されるのでExceptionがでなくなるというわけです。

つまり、今回の場合はWidgetのビルドが完了した後に、Futureでくくった画面遷移の処理が実行されます。

サンプルコード

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({required this.title});

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Text(
          'MyHome Page',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.push(context, MaterialPageRoute(builder: (context) {
            return SecondPage();
          }));
        },
        child: Icon(Icons.navigate_next),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

class SecondPage extends StatefulWidget {
  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  @override
  void initState() {
    super.initState();
    Future(() {
      Navigator.push(context, MaterialPageRoute(builder: (context) {
        return ThirdPage();
      }));
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Second'),
      ),
      body: Center(
        child: Text('Second Page'),
      ),
    );
  }
}

class ThirdPage extends StatefulWidget {
  @override
  _ThirdPageState createState() => _ThirdPageState();
}

class _ThirdPageState extends State<ThirdPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Third'),
      ),
      body: Center(
        child: Text('Third Page'),
      ),
    );
  }
}

結果

無事initStateで画面遷移することができました!

さいごに

よく考えれば確かにって思いますが、気にせず作っているとこういう問題にぶち当たりますよね?

おすすめ参考書

コメント

  1. order cialis より:

    Excellent post. I am going through a few of these issues as well..

  2. Fabulous, what a weblog it is! This weblog provides valuable information to us, keep it up.

タイトルとURLをコピーしました