Flutterで画面遷移の際に渡した引数を遷移先で1回だけ受け取る(buildメソッド以外で受け取る)方法

FlutterでPage1からPage2に遷移する際の引数の渡し方としてググるとよく出てくるのが、次の方法です。

// ルーティング定義
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        initialRoute: '/',
        routes: {
          '/': (context) => Page1(),
          '/page2': (context) => Page2(),
        });
  }
}
// Page1(遷移元)
await Navigator.pushNamed(
  context,
  '/page2',
  arguments: 'argument_from_page1', // Page2への引数
)

//Page2(遷移先)
@override
Widget build(BuildContext context) {
  // Page1からの引数の取得。buildメソッドの中で受け取る。
  _argumentsFromPage1 = ModalRoute.of(context)?.settings.arguments as String;

基本的にはこれで事足りることが多いのですが、このやり方はbuildメソッド限定の取得方法です。
つまり、画面更新ごとに呼ばれるため、それでも問題ないような処理の場合に限ります。

私のケースでは、「来期アニメ」アプリのタグ編集画面を作る際、引数で取得した現在選択中のタグをPage2のメンバ変数にコピーして、その編集結果のデータをPage1へ返す処理を実装する際、このbuildメソッド限定であることが問題となりました。
つまり、タグをタップ>setState>build>メンバー変数へ再コピーされて、タグ編集がリセットされるのです。
このような場合は、メンバ変数への引数のコピーは1回だけとしたいところです。

前段が長くなりましたがその方法を調べました。

パターン1

ルーティング定義のコンストラクタに直接引数を渡す方法です。

// ルーティング定義
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        initialRoute: '/',
        routes: {
          '/': (context) => Page1(),
          '/page3': (context) =>
              // Routing定義でWidgetのコンストラクタに引数を渡す
              Page3(arguments: ModalRoute.of(context)?.settings.arguments),
        }
    );
  }
}

// Page1(遷移元)
await Navigator.pushNamed(
  context,
  '/page3',
  arguments: 'argument_from_page1',
);

// Page3(遷移先)
class Page3 extends StatefulWidget {
  final arguments;
  
  // コンストラクタでPage1からの引数を受け取る
  Page3({this.arguments});

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

class _Page3State extends State<Page3> {
  String _argumentsFromPage1 = '';

  void initState() {
    super.initState();
    // Page1からの引数をメンバ変数に1回だけコピーする
    _argumentsFromPage1 = widget.arguments;
  }

  @override
  Widget build(BuildContext context) {
    // _argumentsFromPage1を使った処理を行う

パターン2

パターン1の「routes」を「onGenerateRoute」に差し替える方法です。
Flutter Cookbookでも推奨している方法とのことです。

// ルーティング定義
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      initialRoute: '/',
      // routesの代わりにonGenerateRouteを使う
      onGenerateRoute: (RouteSettings settings) {
        print('build route for ${settings.name}');
        var routes = <String, WidgetBuilder>{
          '/': (context) => Page1(),
          '/page3': (context) => Page3(arguments: settings.arguments),
        };

        WidgetBuilder builder = routes[settings.name] ?? routes['/']!;

        return MaterialPageRoute(builder: (ctx) => builder(ctx));
      },
    );
  }
}

// Page1(遷移元)、Page3(遷移先)ともにパターン1と同じ

パターン3

Navigator.pushNamedではなく、Navigator.of(context).pushを使います。
pushNamedを使うという前提が崩れていますがこれでもいけました。

// ルーティング定義
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        initialRoute: '/',
        routes: {
          '/': (context) => Page1(),
          '/page2': (context) => Page2(),
          '/page3': (context) => Page3(),
        });
  }
}

// Page1(送信元)
// Navigator.pushNamedではなくpushを使う
Navigator.of(context).push(MaterialPageRoute(
    builder: (context) =>
        Page3(arguments: 'argument_from_page1')));

// Page3(遷移先)はパターン2と同じ

パターン4

トリッキーですが、Future.delayed(Duration.zero)を使って遷移先で解決する方法あるようです。
理屈はよくわかりませんが、JavaScriptで稀にやっていたsetTimeout(function() {}, 0)と同じ匂いがしますが気のせいでしょうか。

// ルーティング定義
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        initialRoute: '/',
        routes: {
          '/': (context) => Page1(),
          '/page4': (context) => Page4(),
        });
  }
}

// Page1(遷移元)※パターン1と同じ
await Navigator.pushNamed(
  context,
  '/page4',
  arguments: 'argument_from_page1',
);

// Page4(遷移先)
class _Page4State extends State<Page4> {
  String? _argumentsFromPage1;

  @override
  void initState() {
    super.initState();
    // future that allows us to access context. function is called inside the future
    // otherwise it would be skipped and args would return null
    // Futureの中ではcontextにアクセスできるらしい。
    Future.delayed(Duration.zero, () {
      setState(() {
        _argumentsFromPage1 =
            ModalRoute.of(context)?.settings.arguments as String;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(

まとめ

パターン1からパターン4のどの方法を使っても課題は解決できました。

パターン3はルーティング定義が意味をなさなくなるので除外、パターン4はトリッキーなので除外、とすると、素直にFlutter Cookbookも推奨のパターン2とするか、よりシンプルなパターン1とするか迷うところです。

とりあえず今回は、パターン1で簡単に済ませました。
また同じケースが発生したときに改めてどのケースを使うか考えようと思っています。

内容がどこまで正しいか自信がないので間違いがあれば教えていただけると助かります。

本記事は以下のページを元に検証、作成しました。

Flutter: Get passed arguments from Navigator in Widget's state's initState
I have a StatefulWidget which I want to use in named route. I have to pass some arguments which I am doing as suggested in i.e.

お役に立てたようであれば、「来期アニメ」アプリのインストールにご協力ください。

来期アニメ

iOS
https://apps.apple.com/jp/app/id1569583513

Android
https://play.google.com/store/apps/details?id=com.raiki_anime

コメント

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