BPSの福岡拠点として一緒にお仕事をさせていただいています、株式会社ウイングドアのウメバヤシです。
最近は主にFlutterでのアプリ開発に携わっております。
複数の画面を一気に戻りたい場合に、おもむろにpopUntil
メソッドを呼び出した時でした。
Bad state: No element
となって戻れずに、「あれ?」ってなったことがあったので、
今回はそのときの解決法を紹介しようと思います。
先に結論
結論から言うと、今回の原因はpushした時にRouteSettings
を指定していなかったことが原因でした。
参考: RouteSettings
class - widgets library - Dart API
ほとんどのルーティングをMaterialApp
のonGenerateRoute
で作成して渡していたので、
その際にルートに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するような実装になっていました。
この場合predicate
はpopUntil
に渡している引数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サイト制作を中心に、
スマホアプリや業務系システムなど様々なシステム開発を承っています。