システム開発部のTです。
前回、Flutterの勉強ということで、画面テーマ〜レイアウト作成まで掲載したかと思います。今回は、Flutterでの基本的な画面遷移の実装と、簡単な画面遷移のアニメーションについて書いていきたいと思います。

以前の記事はFlutter掲載シリーズを参照ください。


準備

プロジェクトを新規で作成後、最初から作成済みのmain.dartを開いてください。main()関数を先頭に、MyAppクラス、MyHomePageクラスと続いているかと思います。

とりあえず、MyHomePageクラス以降をすべて削除し、main()関数、MyAppクラスのみ残すようにコードを編集してください。

最終的には、以下のみになります。

main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

ここまで出来たところで、遷移用の画面を準備します。
本件では、FirstPage、NextPageの2画面を用意します。

FirstPage.dart

import 'package:flutter/material.dart';

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.all(32.0),
        child: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(onPressed: () => {}, child: Text('Nextページへ'),)
            ],
          ),
        ),
      ),
    );
  }
}

NextPage.dart

import 'package:flutter/material.dart';

class NextPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('NextPage'),
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.all(32.0),
        child: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(onPressed: () => {}, child: Text('Firstページに戻る'),)
            ],
          ),
        ),
      ),
    );
  }
}

ここまでが準備になります。
以降、上記のコードを踏まえて画面遷移の実装を進めていきたいと思います。


画面遷移の実装

画面遷移の実装に入りますが、まずmain.dartを開き、以下のhomeの定義を変更してください。これで初期画面としてFirstPageが開きます。

//      home: MyHomePage(title: 'Flutter Demo Home Page'),
     home: FirstPage(),

次にFirstPage.dartを開き、RaisedButtonのonPressedに画面遷移するためのコードを記述しましょう。

Navigator.push(
    context, 
    MaterialPageRoute(builder: (context)=>NextPage(),)
)

上記を追加し、最終的に以下のコードになるかと思います。

FirstPage.dart

import 'package:flutter/material.dart';
import 'package:quiz_app/NextPage.dart';

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.all(32.0),
        child: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(onPressed: () => {
                Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context)=>NextPage(),)
                )
              }, child: Text('Nextページへ'),)
            ],
          ),
        ),
      ),
    );
  }
}

上記のように実装後、アプリを起動するとFirstPageが表示され、「Nextページへ」ボタンを押下することで、NextPageが表示されるかと思います。

NextPage表示後、OSの戻るボタンを押下することで、呼び出し元のFirstPageに戻ることを確認してください。

画像はiOS、Androidそれぞれの画面結果です。


Navigatorについて

Navigatorは画面遷移するときに使用します。
次画面に遷移するときは、Navigator.push()メソッドを使います。

Navigator.push(
    context, 
    MaterialPageRoute(builder: (context)=>NextPage(),)
)

ここで、pushメソッドの第2引数に遷移先を指定しますが、遷移先はMaterialPageRouteでインスタンス化したものを定義する必要があります。

逆に、遷移先から遷移元に戻る場合は、Navigator.pop()を利用することで、
意図したタイミングで戻ることが可能となります。

Navigator.pop(context)

また、呼び出し元が存在するかの確認は、以下のNavigator.canPop()で判定可能です。

Navigator.canPop(context)

上記にて、呼び出し元が存在しない場合は、以下でアプリを終了することができます(Androidのみ)。

SystemNavigator.pop()

上記をNextPageの「Firstページに戻る」ボタン押下時のイベント処理に設定します。

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class NextPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('NextPage'),
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.all(32.0),
        child: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(onPressed: () => {
                if (Navigator.canPop(context)) {  // 呼び出し元が存在するか?
                  Navigator.pop(context)  // 呼び出し元に戻る
                } else {
                  SystemNavigator.pop()  // アプリを終了する
                }
              }, child: Text('Firstページに戻る'),),
            ],
          ),
        ),
      ),
    );
  }
}

ここまでで再度アプリを実行してみましょう。

いかがでしょうか。「Firstページに戻る」ボタンをタップすることで、FirstPageに戻れたかと思います。OSの戻るボタンでも、もちろん呼び出し元の画面に戻れますが、意図的に実装したい場合は、上記のpopを利用することが可能です。

push、popときて、感のいい人であれば分かるかと思いますが、
Flutterの画面遷移の管理はStackでの管理となっています。
後に入れたものを先に出す(後入れ先出し)ので、次の画面に遷移し、一つ前の画面に戻るが基本となります。

では、呼び出し元に戻りたくない場合や、複数ページに遷移したあとに一気に最初のページに戻る場合のパターンはどうするか?そういうのも用意されています。


画面を入れ替える

画面入れ替えの処理でNavigator.pushReplacement()というのがございます。
Navigator.push()と同じように画面遷移しますが、pushReplacementの名前にもあるように画面を入れ替えます。つまり、Stackでいう「入れる」ではないため、現在の画面と入れ替える処理になります。つまり元の画面はpopで戻れなくなります。

例として、先程作成したFirstPageに画面を入れ替える処理を追加してみます。

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.all(32.0),
        child: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(onPressed: () => {
                Navigator.push(
                    context,
                    MaterialPageRoute(builder: (context)=>NextPage(),)
                )
              }, child: Text('Nextページへ(戻れる)'),),
              RaisedButton(onPressed: () => {  // ここのボタンに入替処理を追加
                Navigator.pushReplacement(
                    context,
                    MaterialPageRoute(builder: (context)=>NextPage(),)
                )
              }, child: Text('Nextページへ(戻れない)'),),
            ],
          ),
        ),
      ),
    );
  }
}

実行してみます。

いかがでしょう。
こんな感じに戻れなくなっているのが分かるかと思います。
基本的な画面遷移はpushですが、必要に応じてpushReplacementも利用してみてください。


ルーティングをつかった画面遷移

MaterialAppにあらかじめルーティングの設定をすることで、画面パスを設定して画面遷移をすることができるようになります。

とりあえず例を。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      initialRoute: '/',  // ここ以降の定義がルーティング定義
      routes: {
        '/': (context) => FirstPage(),
        '/next': (context) => NextPage(),
      },
      // home: FirstPage(), ルーティング定義時、homeは削除する
    );
  }
}

上記のように、routesに画面パスと画面クラスを紐付けるように定義しておきます。気をつける点として、homeは削除しておきましょう。
このように定義し、以下で呼び出すことができます。

Navigator.pushNamed(context, '/next')  // 画面を呼び出す
Navigator.pushReplacementNamed(context, '/next')  // 画面を入れ替える

上記を踏まえて、先程のFirstPageのコードを置き換えると・・・、

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
        centerTitle: true,
      ),
      body: Container(
        padding: EdgeInsets.all(32.0),
        child: Center(
          child: Column(
            children: <Widget>[
              RaisedButton(onPressed: () => {
                Navigator.pushNamed(context, '/next')
              }, child: Text('Nextページへ(戻れる)'),),
              RaisedButton(onPressed: () => {
                Navigator.pushReplacementNamed(context, '/next')
              }, child: Text('Nextページへ(戻れない)'),),
            ],
          ),
        ),
      ),
    );
  }
}

どうでしょう。
同じ処理でも、実装がシンプルになりました。
あらためて画面の動きも見てみましょう!
左がAndroid、右がiOSです。
同じように画面遷移しているのが分かるかと思います。

また、戻るときの遷移についても、今までどおりNavigator.pop()を利用することも可能ですが、以下のNavigator.popUntil()を利用することで、パスを直接指定し、その画面まで一気に戻ることも可能になります。

Navigator.popUntil(context, ModalRoute.withName('/'))

これなら、最初の画面から何度も画面遷移した場合にも、楽に実装できますね。

個人的には、最初にルーティングを決めたあとに、画面の実装を進めたほうが、実装も捗るのではと思っています。


画面遷移のアニメーション

ここまで実装していって、お気づきになった方もいるのでは?と思っていますが、AndroidとiOSで画面遷移のアニメーションが異なっていますよね。AndroidとiOSでは、基本的なUIの考え方が違うので、アニメーションが異なることについては問題ございません。

しかし、どちらかに寄せたいとは、カスタムしたいとかっていう要望ってあったりするかと思います。

最後に、このアニメーションについてお話できればと思います。

まず、どこでアニメーションの定義ができるかというと、MaterialAppのthemeで出来ます。themeを設定するために利用するThemeDataにpageTransitionsThemeがあるので、そこに定義することでアニメーションを変更することが可能です。でも、その前にデフォルトでは何が設定されているのかをコードを追ってみてみましょう。以下がそのテーマになります。

@immutable
class PageTransitionsTheme with Diagnosticable {
  /// Construct a PageTransitionsTheme.
  ///
  /// By default the list of builders is: [FadeUpwardsPageTransitionsBuilder]
  /// for [TargetPlatform.android], and [CupertinoPageTransitionsBuilder] for
  /// [TargetPlatform.iOS] and [TargetPlatform.macOS].
  const PageTransitionsTheme({ Map<TargetPlatform, PageTransitionsBuilder> builders }) : _builders = builders;

  static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
    TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
    TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
    TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
    TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
  };

TargetPlatform.androidとか、TargetPlatform.iOSとか・・・、この辺にデフォルトの画面遷移の内容があります。動きをカスタマイズしたいので、一旦上記の定義を直接pageTransitionsThemeに当て込んでみましょう。
コード的には以下の内容になりますね。

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
        pageTransitionsTheme: const PageTransitionsTheme( //ここを追加
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
            TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
            TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
          },
        ),
      ),
      initialRoute: '/', 
      routes: {
        '/': (context) => FirstPage(),
        '/next': (context) => NextPage(),
      },
    );
  }
}

試しにAndroidをiOS風に画面アニメーションを変更してみます。
その場合、TargetPlatform.androidにiOSと同様「CupertinoPageTransitionsBuilder()」を定義しましょう。

        pageTransitionsTheme: const PageTransitionsTheme( 
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: CupertinoPageTransitionsBuilder(),//←変更
            TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
            TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
          },
        ),

ここまで実装したら、Androidで再確認してみましょう!
iOSと同じ動きになったかと思います。

上記を利用することで、iOSをAndroid風に設定することも可能になります。
MaterialAppで定義することで、画面遷移のアニメーションを変更できることは理解できたかと思います。

本件ではここまでとしますが、各画面遷移ごとにアニメーションを変更することも可能です。その実装手段については、後日記載していければと思います。


まとめ

本件では、画面遷移を中心に、基本的な画面遷移時の実装、アニメーションについてお話しました。最後に復習していきます。

  • Navigatorでの画面遷移
    画面遷移はStack(後入れ先出し)管理されている。
    画面遷移は基本的に進むはpush、戻るはpopを利用する
    画面の入れ替えは、pushReplacementで可能
  • ルーティング定義
    MaterialAppのroutesにパス名と画面クラスの紐付けを定義することで、画面遷移時にパス名に紐付けた画面を呼び出すことが可能。
    popするときも、popUntilを利用することで、直接その画面に戻ることが可能となり、何度もpopする必要がなくなる
  • 画面遷移アニメーション
    MaterialAppのtheme内のパラメータpageTransitionsThemeに対してアプリ全体の画面遷移アニメーションを定義することが可能
    AndroidをiOS風に、iOSをAndroid風に切り替えが可能

以上になりますが、画面遷移についてはまだまだ話足りないこともあるので、
次回も引き続き画面遷移の話ができればと思います。