パルカワ2

PerlとRubyとイチャラブ

より良いFlutterの画面遷移を考える

Flutterの画面遷移について考えたのでメモ。 現状このようにして遷移している。

Navigator.of(context).push(MaterialPageRoute(
  builder: (_) => HogeScreen(keyword: "..."),
));

このやり方だと settings.name が設定されていないので、FirebaseAnalyticsにログを送るときになんの画面なのか判別できないためログが送られない

settings.name を設定する

FirebaseAnalyticsObserverを利用して画面遷移のログを送信したい場合、settings.nameがnullだとログが送られない。 FirebaseAnalyticsObserver class - observer library - Dart API

ログを送るためには以下のようにsettings.name を設定するか、pushNamed を使う.

Navigator.of(context).push(MaterialPageRoute(
  settings: RouteSettings(name: "HogeScreen"),
  builder: (_) => HogeScreen(keyword: "..."),
))

pushNamed を使う

pushNamed は以下のように使い、画面遷移のコードを何度も書く必要がないというメリットもある。 pushNamed method - Navigator class - widgets library - Dart API

Navigator.of(context).pushNamed("HogeScreen", arguments: "...")

しかし、pushNamedには以下のような問題もあると思った。

  • route name を String で渡しそれを利用してRoute(またはWidget)を作るので、うっかりミスが起きやすい
  • settings.arguments 経由で値を渡すことになるので、型がObjectになり利用するときにcastする必要がありうっかりミスが起きやすい
  • route nameだけでは arguments が必要かどうかわからないので、うっかりミスが起きやすい

人間のうっかりミスが起きやすく、コンパイルエラーではなく実行時エラーになるため最悪気づかないことがある。

route メソッドをつくる

pushNamedは画面遷移のコードを何度も書く必要がないという点では便利だけど、うっかりミスも起きやすいと感じた。 それを防ぐためにはこのようにRouteを生成するメソッドを書いてあげればよいのではと考えた。

extension HogeScreenRoutes on HogeScreen {
  static Route<dynamic> route({@required this.keyword})) {
    return MaterialPageRoute(
      settings: RouteSettings(name: "HogeScreen"),
      builder: (_) => HogeScreen(keyword: "..."),
    )
  }
}
Navigator.of(context).push(HogeScreen.route());

こうすればうっかりタイポなどをしても実行したタイミングでエラーになるのではなくコンパイルのタイミングでわかるので、最高。 ただし、これにもうっかりミスが起きる場所がある。HogeScreenに渡す値が増えたときだ。

class HogeScreen {
  final String keyword;
  final String fuga; // 増えた
  HogeScreen({Key key, @required this.keyword, @reuqired this.fuga});
}

extension HogeScreenRoutes on HogeScreen {
  static Route<dynamic> route({@required String keyword})) {
    return MaterialPageRoute(
      settings: RouteSettings(name: "HogeScreen"),
      builder: (_) => HogeScreen(keyword: keyword), // fugaが渡されていない
    )
  }
}

@required をしていてもコンパイルエラーにならない。lintを動かせばエラーにすることはできる。 複数箇所変更するのが嫌ならコンストラクタからrouteメソッドを自動生成すればいいかなと思ったけどそこまでやらなくていいかなと思ってここで考えるのをやめた。 もっといい方法があるなら知りたい。

追記: こうやれば一応一箇所の変更ですむねって話を同僚としたけど、やりすぎ感あるのでやめておいた。

class HogeScreenArguments {
  final String keyword;
  final String fuga;
  HogeScreenArguments({Key key, @required this.keyword, @required this.fuga});
}

class HogeScreen {
  final HogeScreenArguments arguments;
  HogeScreen({Key key, @required this.arguments});
}

extension HogeScreenRoutes on HogeScreen {
  static Route<dynamic> route({@required HogeScreenArguments arguments})) {
    return MaterialPageRoute(
      settings: RouteSettings(name: "HogeScreen"),
      builder: (_) => HogeScreen(arguments: arguments),
    )
  }
}