Flutterでの画面遷移まとめ

Posted by kwmt27 on Thu, Mar 26, 2020

はじめに

先日Flutter製チャットアプリを支える技術でチャットアプリを作ったときの内容を書いて今年は終わりかなと思っていたら、こちらの枠が空いていたので前回触れなかった画面遷移について書きたいと思います。

Flutterの画面遷移についてはNavigation & routingを見ると基本的には良いと思います。

ですが、実際アプリを作った際にこれだけでは足りないと思いますので、画面遷移をiOS・Androidのそれぞれの動画+実装という形でまとめておきたいと思います。

別の画面に遷移したい

iOSAndroid
ios-push.gif
android-nav.gif

たとえば、ルーム一覧画面からパスを"/rooms/<roomId>“とするルーム詳細画面(RoomScreen)に遷移したいときは次のようにします。1

Navigator.push(
  context,
  MaterialPageRoute(
      settings: RouteSettings(name: "/rooms/<roomId>"),
      builder: (BuildContext context) => RoomScreen(roomId)),
);

前の画面に戻りたい

iOSAndroid
ios-nav.gif
android-nav2.gif

Navigator.pushを使って画面遷移したあと、左上のバックボタンをタップして戻る場合は特に処理は必要ありませんが、それ以外のボタンから戻りたい場合もあるかと思います。その場合は戻りたいタイミングで次のようにします。(上の動画では、下部のバツボタンをタップしています)

Navigator.pop(context);

ダイアログを出したい

iOSAndroid

showDialogAlertDialogの組み合わせです。AlertDialogの方のドキュメントにもサンプルが載っています。

Future<bool> showDialogMessage(BuildContext context,
    {String title, String message, bool isOkOnly = false}) {
  return showDialog<bool>(
    context: context,
    builder: (context) =>
        _buildDialog(context, title, message, isOkOnly: isOkOnly),
  );
}
Widget _buildDialog(BuildContext context, String title, String message,
    {bool isOkOnly = false}) {
  if (title == null && message == null) {
    throw ArgumentError("titleとmessageのどちらともnullです。どちらかは指定してください。");
  }
  List<Widget> actions = List();
  if (!isOkOnly) {
    actions.add(FlatButton(
      child: Text(Cancel),
      onPressed: () {
        Navigator.pop(context, false);
      },
    ));
  }
  actions.add(FlatButton(
    child: Text(OK),
    onPressed: () {
      Navigator.pop(context, true);
    },
  ));
  return AlertDialog(
    title: title != null ? Text(title) : null,
    content: message != null ? Text(message) : null,
    actions: actions,
  );
}

前の画面にデータを渡したい

AndroidのonActivityResult的なことをしたいわけですが、まずawaitを付けて別の画面に遷移します。

void _showProfileEditPage(BuildContext context) async {
  User user = await Navigator.push(/* 省略 */).;
  if (user == null) {
    // something
    return;
  }
  // something
}

そして、遷移した先の前の画面にデータを渡したいタイミングでNavigator.of(context).pop(_user) とすると前の画面にデータを渡せます。

この例ではUser型の_userを返してます。

前の画面に戻る前に、戻っていいか確認ダイアログを出しOKの場合のみ戻りたい

iOSAndroid
ios-nav.gif
a-nav.gif

編集画面などでなにか編集して確定する前に間違えてとかで前に戻ろうとしてしまう場合があるかと思います。 そのようなときは、編集中ですが戻っていいか確認ダイアログを出すこともあると思います。

上の動画は、ダイアログを出したあと「Cancel」をタップしたらなにもせず、「OK」をタップしたら前の画面に戻る動画になります。

これをするには、WillPopScopeでWrapします。

WillPopScopeのonWillPopはバックキーイベントを検知してくれるような動作になるので、そこでダイアログを出したりするとよさそうです。

Widget body = WillPopScope(
  onWillPop: _willPopCallback,
  child: Form(
      key: _formKey,
      child: SafeArea(
        child: Container(
          padding: EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              roomImageWidget,
              Expanded(
                child: TextFormField(
                  controller: _controller,
                  onSaved: (String name) {
                    debugPrint(name);
                    _editingRoom.name = name;
                  },
                ),
              )
            ],
          ),
        ),
      )),
);

onWillPop_willPopCallbackは下記のようにするとできます。

Future<bool> _willPopCallback() async {
  if (_textEditController.text != <画面遷移時にTextFieldにセットした値>) {
    return showDialogMessage(context,
            title: <タイトル>,
            message: <メッセージ>) ??
        false;
  }
  // trueで戻る。falseで戻らない
  return true;
}

showDialogMessageは先述した「ダイアログを出したい」で紹介した自前のメソッドです。

モーダルで遷移したい

iOSAndroid
ios-nav.gif
a-nav.gif

左上のボタンをバックボタン(← or <)はなくバツボタン(x)にするには、MaterialPageRoutefullscreenDialogプロパティがある2ので、それをtrueにするだけです。

Navigator.push(
  context,
  MaterialPageRoute(
      settings: RouteSettings(name: "/rooms/<roomId>/edit"),
      builder: (BuildContext context) => RoomScreen(roomId),
      fullscreenDialog: true,
   ),
);

今までの画面を破棄して新しく画面を表示したい

iOSAndroid
ios-nav.gif
a-nav.gif

上の動画はサインイン後にサインイン画面は不要なので破棄しホーム画面に遷移しています。左上にバックボタンやバツボタンがないことがわかるかと思います。

これを実現するにはpushReplacementを使います。

Navigator.of(context).pushReplacement(
  MaterialPageRoute(
      settings: RouteSettings(name: "/home"),
      builder: (BuildContext context) => HomeScreen(),
   ),
);

BottomSheetで選択させたい

iOSAndroid
ios-nav.gif
a-nav.gif

showModalBottomSheetを使います。3

showModalBottomSheet<void>(
    context: context,
    builder: (BuildContext context) => Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            ListTile(
              leading: Icon(Icons.photo_library),
              title: Text("Gallery"),
              onTap: onTapGallery,
            ),
            ListTile(
              leading: Icon(Icons.camera_alt),
              title: Text("Camera"),
              onTap: onTapCamera,
            ),
          ],
        ));

SnackBarを表示したい

iOSAndroid
ios-nav.gif
a-nav.gif
Scaffold.of(context).showSnackBar(SnackBar(content: Text(text)));

画面遷移時にアニメーションしたい

iOSAndroid
ios-nav.gif
a-nav.gif

MaterialPageRouteを継承したクラスを作って、buildTransitionsメソッド内でアニメーションさせたいTransitionウィジェットを返すようにします。4

そして、上で紹介したMaterialPageRouteの部分を継承先のクラスに置き換えてやれば画面遷移時にアニメーションします。

class _FadeAnimationCustomRoute<T> extends MaterialPageRoute<T> {
  _FadeAnimationCustomRoute(
      {WidgetBuilder builder, RouteSettings settings, bool fullscreenDialog = false})
      : super(
            builder: builder,
            settings: settings,
            fullscreenDialog: fullscreenDialog);

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation,
      Animation<double> secondaryAnimation, Widget child) {
    if (settings.isInitialRoute) return child;
    return FadeTransition(opacity: animation, child: child);
  }
}

上の動画はこれを実装したものですが、フェードしてるのわかりますかね😅

おわりに

以上、実際に作成したアプリで必要だった画面遷移に関してまとめてみました。 ご参考になれば幸いです。


  1. この例ではRoomScreenのコンストラクタでroomIdを渡せるようにしています。 ↩︎

  2. 正確にはMaterialPageRouteの親クラスのPageRoutefullscreenDialogプロパティを持っています。 ↩︎

  3. onTapGalleryonTapCameraVoidCallback型です。 ↩︎

  4. チャットアプリの現バージョンでは使っていませんが、次のバージョンに入れる予定です。 ↩︎



comments powered by Disqus