【Flutter】NavigatorのpushReplacementの遷移先画面からpopを使用して、情報を渡す方法

# はじめに

画面遷移して、前の画面に戻る時にpopがよく使われると思います。
またpopの引数に情報や値を入れることで、
戻った後の画面にその情報や値を取得するような使い方もあると思います。

しかし、pushReplacementを用いて遷移した後では、
その取得の仕方に少し工夫がいることに気がつきました。

通常のpushと違って、画面の置き換えが発生するので、
popをすると、前画面に戻らず、その一つ前の画面に遷移されます。
これが原因で通常のやり方では、取得できないようになっています。

pushReplacementを用いて、
その一つ前の画面にpopの引数として渡された値を渡す方法を
今回の記事で紹介したいと思います。


# 使用技術

- macOS 12.4
- Flutter 2.2.3


# 状況例

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/6ef41257-6902-7620-21d4-901c1200214b.png" width=800 />

本記事で取り扱う、見た目や画面の遷移に関して、上記の通りになります。
pushで遷移する画面もイメージしやすいように作成しましたが、
本記事はpushReplacementにフォーカスして、進めていきます。
（pushを使った場合の遷移は図の右上、pushReplacemnetを使った場合は図の右下のものを示します。青矢印の遷移は各々でpopを使用した場合の遷移を表します）

下記がコードの例になります。

```dart:main.dart
import 'package:flutter/material.dart';

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

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

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ホーム画面"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            var result = await Navigator.push(// ・・・①
              context,
              MaterialPageRoute(
                builder: (context) => NavigationButtonScreen(),
              ),
            );
            print("MyHomePage result: $result");
          },
          child: Text("ボタン画面"),
        ),
      ),
    );
  }
}

class NavigationButtonScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text("ナビゲーションボタン"),
          leading: GestureDetector(
            onTap: () {
              Navigator.of(context).pop("NavigationButtonScreenから渡す");
            },
            child: Icon(Icons.arrow_back_ios),
          )),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () async {
                var result = await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushScreen(),
                  ),
                );
                print("Navigator push result: $result");
              },
              child: Text("Navigator push"),
            ),
            ElevatedButton(
              onPressed: () async {
                var result = await Navigator.pushReplacement(// ・・・②
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushReplacementScreen(),
                  ),
                );
                print("Navigator pushReplacement result: $result");
              },
              child: Text("Navigator pushReplacement"),
            ),
          ],
        ),
      ), 
    );
  }
}

class NavigatorPushScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Navigator.pushを使用"),
        leading: GestureDetector(
          onTap: () {
            Navigator.of(context).pop("渡す");
          },
          child: Icon(Icons.arrow_back_ios),
        ),
      ),
      body: Center(
        child: Text("Navigator.pushを使用"),
      ),
    );
  }
}

class NavigatorPushReplacementScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Navigator.pushReplacementを使用"),
        leading: GestureDetector(
          onTap: () {
            Navigator.of(context).pop("ホーム画面に渡せない");
          },
          child: Icon(Icons.arrow_back_ios),
        ),
      ),
      body: Center(
        child: Text("Navigator.pushReplacementを使用"),
      ),
    );
  }
}

```

# 問題点の再現と確認

再現のために遷移を確認していきます。
pushReplacementを使用した場合の返り値の返ってくるタイミングやその中身について見ていきます。


``` dart: ホーム画面
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ホーム画面"),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () async {
            var result = await Navigator.push(// ・・・①
              context,
              MaterialPageRoute(
                builder: (context) => NavigationButtonScreen(),
              ),
            );
            print("MyHomePage result: $result");
          },
          child: Text("ボタン画面"),
        ),
      ),
    );
  }
}

```
アプリ側の表示
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/b06be237-2a33-4393-3782-025c82fe81ad.png" width=300 />

MyHomePageウィジェットがこのアプリにおける初めに表示される画面となります。
ここからスタートします。
『ボタン画面』と書かれたボタンを押下すると、
Navigatorの遷移の仕方を選択できる画面を指すNavigationButtonScreenに遷移します。
遷移先画面から、pop使用された際、引数に値が入ってた場合は、ElevatedButtonのonPressedメソッドの中にある、

``` dart
var result = await Navigator.push(// ・・・①
              context,
              MaterialPageRoute(
                builder: (context) => NavigationButtonScreen(),
              ),
            );
```

変数resultにその値が入ります。
通常のpushを使用して、遷移された画面であれば、
遷移先の画面からpopを用いて値を遷移元の画面に渡せます。

そして、次の遷移先のボタン画面を見ていきます。

``` dart: ボタン画面
class NavigationButtonScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
          title: Text("ナビゲーションボタン"),
          leading: GestureDetector(
            onTap: () {
              Navigator.of(context).pop("NavigationButtonScreenから渡す");
            },
            child: Icon(Icons.arrow_back_ios),
          )),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            ElevatedButton(
              onPressed: () async {
                var result = await Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushScreen(),
                  ),
                );
                print("Navigator push result: $result");
              },
              child: Text("Navigator push"),
            ),
            ElevatedButton(
              onPressed: () async {
                var result = await Navigator.pushReplacement(// ・・・②
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushReplacementScreen(),
                  ),
                );
                print("Navigator pushReplacement result: $result");
              },
              child: Text("Navigator pushReplacement"),
            ),
          ],
        ),
      ), 
    );
  }
}

```

アプリ側の表示
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/830d6e8f-994a-477d-6485-49da354ee722.png" width=300 />

先ほどのホーム画面から遷移されたボタン画面になります。
ウィジェットでは、NavigationButtonScreenを指します。

``` dart: ボタン画面

var result = await Navigator.pushReplacement(// ・・・②
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushReplacementScreen(),
                  ),
                );

```

こちらでは、上記のpushReplacementを実行する『Navigator pushReplacement』というボタンを押下すると、

まず、先ほどのホーム画面に存在するMyHomePageウィジェットに仕込んであった変数resultが返ってきます。


```dart: ホーム画面

ElevatedButton(
          onPressed: () async {
            var result = await Navigator.push(// ・・・①
              context,
              MaterialPageRoute(
                builder: (context) => NavigationButtonScreen(),
              ),
            );
            print("MyHomePage result: $result");
          },
          child: Text("ボタン画面"),
        )

```
ログでのprintの表示
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/b917d81d-3518-7096-5be5-dbe07eb776de.png" width=300 />

値を渡してpopを実行していないため、変数resultの中身はnullになります。

pushReplacementを用いて遷移された画面を見ていきます。


``` dart: pushReplacementで遷移した画面
class NavigatorPushReplacementScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Navigator.pushReplacementを使用"),
        leading: GestureDetector(
          onTap: () {
            Navigator.of(context).pop("ホーム画面に渡せない");
          },
          child: Icon(Icons.arrow_back_ios),
        ),
      ),
      body: Center(
        child: Text("Navigator.pushReplacementを使用"),
      ),
    );
  }
}

```

アプリ側の表示
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/0aad02bf-e6e2-d302-a573-854bd6238082.png" width=300 />

先ほどのボタン画面から遷移される画面になります。
ウィジェットでは、NavigatorPushReplacementScreenを指します。
ここで、ついにAppbarのleadingプロパティで使用されているGestureDetectorのonTapの中身の関数である

```dart: pushReplacementで遷移した画面
Navigator.of(context).pop("ホーム画面に渡せない");
```

を実行すると、
まずアプリ上では、

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/c234af5f-d6e7-fe1e-a3bc-0146f9cce6eb.png" width=300 />

一つの前の遷移元のボタン画面には戻らず、
そのさらに一つ前、つまり二つ前である、ホーム画面に戻りました。
ボタン画面でpushReplacementを使用したため、画面が置き換えが発生し、ボタン画面へは戻れなくなります。

printを仕込んだログ上を確認してみると、

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/8e420375-a486-3ea2-9e99-3177bafbc8c3.png" width=300 />

表示されている画面とは反して、

置き換えで消えたボタン画面を指す、NavigationButtonScreenの中にある

```dart: ボタン画面

ElevatedButton(
              onPressed: () async {
                var result = await Navigator.pushReplacement(// ・・・②
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushReplacementScreen(),
                  ),
                );
                print("Navigator pushReplacement result: $result");
              },
              child: Text("Navigator pushReplacement"),
            ),

```

ElevatedButtonの中のresultが返ってきて、
printが発火されます。

ホーム画面に戻ったのに、ボタン画面の方に返り値が返ってくる、
ホーム画面の返り値はpushReplacementを実行されたタイミングで返ってきたりなど、
このように画面の見た目とpushReplacementメソッドの返り値が送られるタイミングに相違があります。

なので今回は、遷移から戻ってきたホーム画面にpopをした値を返したいと思います。
ホーム画面の返り値はpushReplacementを実行されたタイミングで返ってくるのを避けていくようにしていきます。


# 解決策

Completerを使用します。
NavigationButtonScreenのNavigator pushReplacementを表示するElevatedButtonを下記のように置き換えます

```diff_dart: ボタン画面

ElevatedButton(
              onPressed: () async {
+                final completer = Completer();
                var result = await Navigator.pushReplacement(// ・・・②
                  context,
                  MaterialPageRoute(
                    builder: (context) => NavigatorPushReplacementScreen(),
                  ),
+                  result: completer.future,
                );
+                completer.complete(result);
                print("Navigator pushReplacement result: $result");
              },
              child: Text("Navigator pushReplacement"),
            )

```

completerを使うことで、遷移元の画面にも値が伝わるようになります。

一連のフローをもう一度実行してみて、
ログ上のprintを確認してみると、

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/927693/c29c36d8-ee2b-a2b4-417e-f360b313445c.png" width=300 />

pushReplacementが実行されたタイミングではなく、
無事にホーム画面にpopに渡した値をresultとして返すことができました。


---


# おわり


以上、『【Flutter】NavigatorのpushReplacementの遷移先画面からpopを使用して、情報を渡す方法』についての記事でした。
本記事が誰かの助けになれば嬉しいです。
最後までご覧いただきありがとうございました。


# 参考資料


https://www.technicalfeeder.com/2021/11/flutter-page-transition/

https://api.flutter.dev/flutter/dart-async/Completer-class.html