【Flutter】AndroidでiOS風の画面遷移とスワイプによるバックを実現する

# はじめに
Flutterはクロスプラットフォームなフレームワークですが、Flutterでモバイルアプリを開発していて、Android・iOSという二つのOSで画面遷移時の処理を統一するのに少し工夫が必要だったため共有します。
本記事ではAndroidにおいてiOS風の画面がスライドするようなトランジションエフェクトと、スワイプによる画面バック処理の実装について紹介します。
Androidバックキー押下時に何か処理を実装していた場合、そのままではスワイプによる画面バック動作が無効になってしまうので、その解決を試みました。
 
 # 目標
 AndroidでiOS風のトランジションと「スワイプで前の画面に戻る」動作を共通で実装させる。

 # 実装

 実装の内容は大まかに以下となります。

 1\. AndroidでiOS風の画面遷移をさせる
 2\. バックキーに処理を設定する
 3\. バックキー押下時の処理とスワイプによるバック動作を併存させる

 ## 1\.AndroidでiOS風の画面遷移をさせる


 Flutterのコードを読むと、以下のようにOSによって異なる画面遷移テーマが設定されていることがわかります。

https://github.com/flutter/flutter/blob/7048ed95a5/packages/flutter/lib/src/material/page_transitions_theme.dart#L668


```dart:PageTransitionsTheme
static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
  TargetPlatform.android: ZoomPageTransitionsBuilder(),
  TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
  TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
};
```

 ここから、AndroidでiOS風の画面遷移を実現するためには、Androidに対してもiOSと同様のテーマである`CupertinoPageTransitionsBuilder`をすれば良いとわかります。
 よってMaterialAppのbuildThema内で、たとえば次のように実装します。

 ```dart:sampleコード
//main.dart内を抜粋
return MaterialApp(
  theme: ThemeData(
  pageTransitionsTheme: const PageTransitionsTheme(
    builders: <TargetPlatform, PageTransitionsBuilder>{
      TargetPlatform.android: CupertinoPageTransitionsBuilder(),//Androidの遷移もIOS風に指定している
      TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
    },
  ),
  title: 'SAMPLE',
  locale: localeJP,
);
 ```

これでAndroidでiOSと同じ画面遷移を行うことができました。
続いてAndroidにおけるバックキーの動作を実装していきましょう。

## 2\. バックキーに処理を設定する

今回、Androidのバックキーを押したときに特定の処理を実行させたかったのでこちらを実装していきます。

デフォルトではバックキーを押下したとき画面をバックする動作が実行されるだけなので、任意の処理を実行させるため、`WillPopScope`というウィジェットを使用します。

`WillPopScope`はchildプロパティに指定したウィジェットに対して、画面が戻る動きを検知して、そのタイミングで実行されるコールバックを設定できるウィジェットになります。
（正確には画面が破棄されることを拒否するコールバックを登録できると公式のドキュメントに説明されています


https://api.flutter.dev/flutter/widgets/WillPopScope-class.html


次のように`WillPopScope`でウィジェットをラップすることで、Androidのバックキー押下時に実行されるコールバック処理を登録することができます。

```dart:sampleコード
WillPopScope(
  onWillPop: () async { //コールバックはFuture<bool>で設定する
    /*
      バックキー押下時に実行させたい処理
    */
    return true; //trueなら処理実行後画面を閉じる
  },
  child: Scaffold(
    body: //画面を構成するWidget
  ),
)
```

`WillPopScope`は画面をバックさせたくないときにも使用できますが、今回はAndroidのバックキー押下時の処理を設定するために利用します。


## 3\. バックキー押下時の処理とスワイプによるバック動作を併存させる

ここまででAndroidにおけるiOS風の画面処理と、バックキー押下時に実行される処理の設定を行いました。
この状態では、実はスワイプによる画面バック機能が無効になっています。
その理由はFlutterにおける画面遷移の基礎となっている`ModalRoute`クラスにあります。

Flutterのソースコード上を見ていくと`ModalRoute`クラスに`hasScopedWillPopCallback`というgetterが存在することがわかります。

https://github.com/flutter/flutter/blob/7048ed95a5/packages/flutter/lib/src/widgets/routes.dart#L1613


こちらのコメントを読むと、

> This method is used to disable the horizontal swipe pop gesture supported
> by [MaterialPageRoute] for [TargetPlatform.iOS] and
> disabled.

とあり、WillPopScopeCallbackが複数個あるとき、スワイプでバックするジェスチャが非活性化されていることがわかります。

ここで、Flutterにおける最もシンプルな画面遷移処理の例を次に示します。

```dart:シンプルな画面遷移処理
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => SecondPage()),
);
```

この`MaterialPageRoute`クラスは、先ほど言及した`ModalRoute`クラスを継承しているため、`MaterialPageRoute`のサブクラスを作れば`hasScopedWillPopCallback`をオーバーライドすることができます。

したがって、スワイプによるバック動作の非活性化を防ぐためには次のようにカスタムしたクラスを定義し、使用します。

```dart:CustomMaterialPageRoute
class CustomMaterialPageRoute extends MaterialPageRoute {
  CustomMaterialPageRoute(
    {
      required WidgetBuilder builder,
      required RouteSettings settings,
    }): super(builder: builder, settings: settings);

  @override
  bool get hasScopedWillPopCallback => false; //WillPopScopeがあるときもスワイプによるバックを許容する
}
```

`MaterialApp`内での使用例を次に示します。

```dart:sampleコード
return MaterialApp(
  theme: ThemeData(
    pageTransitionsTheme: const PageTransitionsTheme(
      builders: <TargetPlatform, PageTransitionsBuilder>{
        TargetPlatform.android: CupertinoPageTransitionsBuilder(),//Androidの遷移もIOS風に指定している
        TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
      },
  ),
  title: 'SAMPLE',
  locale: localeJP,
  onGenerateRoute: (RouteSettings settings) {
    swich(settings.name){
      case: '/SecondPage'{
        return CustomMaterialPageRoute(
          builder: ((context) => SecondPage()),
          settings: settings,
        );
      }
      //以下ルート名別に処理を設定する        
    },
  );
);
```

これで`WillPopScope`で設定したコールバックとスワイプによる画面バック動作を併存させることができました。


# おわりに
今回はFlutterで
- Android上でもiOS風の画面遷移を実現する
- スワイプによる画面バック動作を保持したままバックキー押下時に処理を設定する
という二つの処理についてご紹介しました。
閲覧ありがとうございました。