Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連以外

Flutter: popUntilで「Bad state: No element」となった時に確認すること

BPSの福岡拠点として一緒にお仕事をさせていただいています、株式会社ウイングドアのウメバヤシです。

最近は主にFlutterでのアプリ開発に携わっております。

複数の画面を一気に戻りたい場合に、おもむろにpopUntilメソッドを呼び出した時でした。
Bad state: No elementとなって戻れずに、「あれ?」ってなったことがあったので、
今回はそのときの解決法を紹介しようと思います。

先に結論

結論から言うと、今回の原因はpushした時にRouteSettingsを指定していなかったことが原因でした。

参考: RouteSettings class - widgets library - Dart API

ほとんどのルーティングをMaterialApponGenerateRouteで作成して渡していたので、
その際にルートにRouteSettingsを渡しておらず、popUntilで戻る際にページを見つけることができずにエラーになっていたようです。

例えば以下の様な実装になっていると、

const String pageA = '/page_a';
const String pageB = '/page_b';
const String pageC = '/page_c';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: pageA,
      onGenerateRoute: (setting) {
        final routeName = setting.name;

        switch (routeName) {
          case pageA:
            return MaterialPageRoute(
              builder: (context) {
                return PageA();
              },
            );

          case pageB:
            return MaterialPageRoute(
              builder: (context) {
                return PageB();
              },
            );

          case pageC:
            return MaterialPageRoute(
              builder: (context) {
                return PageC();
              },
            );
        }
        return null;
      },
    );
  }
}

PageA→PageB→PageCと遷移させた後にPageCで

Navigator.of(context).popUntil(ModalRoute.withName(pageA));

としても「Bad state: No element」となり遷移できません。

ここでpopUntilメソッドの内部実装を見てみると以下のようになっています。

// navigator.dart

  void popUntil(RoutePredicate predicate) {
    while (!predicate(_history.lastWhere(_RouteEntry.isPresentPredicate).route)) {
      pop();
    }
  }

参考: popUntil method – Navigator class – widgets library – Dart API

関数predicateがfalseを返す間はpopするような実装になっていました。
この場合predicatepopUntilに渡している引数ModalRoute.withName(pageA)のことです。

ここで、ModalRoute.withName(String name)の実装を見てみます。

// routes.dart

  static RoutePredicate withName(String name) {
    return (Route<dynamic> route) {
      return !route.willHandlePopInternally
          && route is ModalRoute
          && route.settings.name == name;
    };
  }

参考: withName method – ModalRoute class – widgets library – Dart API

ここで注目したいのがroute.settings.name == nameの条件です。

つまり、popUntilではModalRoute.withNameの渡した引数のnameと、Navigatorが持っているルートのroute.settings.nameが一致するまでpopするという実装になっています。

前述の実装ではルートを作成する際にsettings.nameを指定していなかったので、Navigatorがルートをみつけられずにエラーとなっていたようです。

この場合、例えば以下の部分を

        switch (routeName) {
          case pageA:
            return MaterialPageRoute(
                builder: (context) {
                  return PageA();
                });

このように修正することで解決することができます。

        switch (routeName) {
          case pageA:
            return MaterialPageRoute(
                settings: RouteSettings(name: pageA), // <--- 追加
                builder: (context) {
                  return PageA();
                });

なぜRouteSettingsを渡していなかったのか

onGenerateRouteでルーティングを作成していた理由として、
名前付きルートで遷移先に値を渡すことがあったからです。
また、その方法として書籍やサンプルなどで紹介されていることが多い実装方法だったからです。

しかし、その際にあまりRouteSettingsのことについては言及されていることが少なく、
popUntilとの組み合わせで今回のエラーが起きたのです。

ちなみにpushNamedメソッドでは第2引数でパラメータを渡すことができます。

Navigator.of(context).pushNamed(pageA, arguments: 'hoge');

例えばそれを先ほどの例のPageAでパラメータを受け取る場合はこう書きます。

        switch (routeName) {
          case pageA:
            return MaterialPageRoute(
                builder: (context) {
                  return PageA(setting.arguments);
                });

複数渡したい場合はクラスを自作して渡します。

// 自作クラス
class PageAArguments {
  PageAArguments({this.hoge, this.fuga});

  String hoge;
  String fuga;
}
// pushNamedで渡す時
Navigator.of(context).pushNamed(
  pageB,
  arguments: PageAArguments(
    hoge: 'hoge',
    fuga: 'fuga',
  ),
);

その他のハマりどころ

PageRouteBuilderで自作のルートを作成している場合も、RouteSettingsを渡すのを忘れがちです。
例えば下からスライドで遷移させるPageRouteは以下のように実装します。

class SlideUpPageRoute extends PageRouteBuilder {
  final Widget child;
  SlideUpPageRoute({this.child})
      : super(
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) {
            return child;
          },
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) {
            return SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0, 1),
                end: Offset.zero,
              ).animate(animation),
              child: child,
            );
          },
        );
}

この実装は、画面遷移時のアニメーションをカスタムする際によく見かける例です。
最初の例のPageBの遷移で使うとすると、onGenerateRouteのパラメータの中で以下のようにして使います。

          case pageB:
            return SlideUpPageRoute(child: PageB());

ただこの時にもルートにRouteSettingsを渡していないので、popUntilなどでPageBのルートを見つけることができません。

この場合はSlideUpPageRouteを以下のように修正することで解決します。

class SlideUpPageRoute extends PageRouteBuilder {
  final Widget child;
  SlideUpPageRoute({this.child, RouteSettings settings})  // <--- RouteSettings settings を追加
      : super(
          settings: settings,  //  <--- RouteSettings をスーパークラスに渡す
          pageBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
          ) {
            return child;
          },
          transitionsBuilder: (
            BuildContext context,
            Animation<double> animation,
            Animation<double> secondaryAnimation,
            Widget child,
          ) {
            return SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0, 1),
                end: Offset.zero,
              ).animate(animation),
              child: child,
            );
          },
        );
}
          case pageB:
            return SlideUpPageRoute(child: PageB(), settings: RouteSettings(name: pageB));

ルーティングを作成する時にはRouteSettingsを渡しておいた方が無難

popUntilなど、ルートを名前指定して戻ったりする系のメソッドを使用する際に、
RouteSettingsがないとページを見つけられずにエラーになるので、
遷移させる時やルートを作成する際には、RouteSettingsを渡しておいた方が無難かなと思いました。



株式会社ウイングドアでは、Ruby on RailsやPHPを活用したwebサービス、webサイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。