Posted at

Flutterでの画面遷移まとめ


はじめに

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

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

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


別の画面に遷移したい

iOS
Android

ios-push.gif
android-nav.gif

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


room_list_screen.dart

Navigator.push(

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


前の画面に戻りたい

iOS
Android

ios-nav.gif
android-nav2.gif

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

Navigator.pop(context);


ダイアログを出したい

iOS
Android

IMG_923E967FABA9-1.jpeg
IMG_923E967FABA9-1.jpeg

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の場合のみ戻りたい

iOS
Android

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は先述した「ダイアログを出したい」で紹介した自前のメソッドです。


モーダルで遷移したい

iOS
Android

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,
),
);


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

iOS
Android

ios-nav.gif
a-nav.gif

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

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

Navigator.of(context).pushReplacement(

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


BottomSheetで選択させたい

iOS
Android

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を表示したい

iOS
Android

ios-nav.gif
a-nav.gif

Scaffold.of(context).showSnackBar(SnackBar(content: Text(text)));


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

iOS
Android

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);
}
}

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


おわりに

以上、実際に作成したアプリで必要だった画面遷移に関してまとめてみました。

ご参考になれば幸いです。





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



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



  3. onTapGalleryonTapCameraVoidCallback型です。 



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