実践Flutter

Flutterでスマホを中心にアプリ制作していきます。

Flutterのページ遷移1・GetXで簡単ページ切り替え

はじめに

本記事はFlutterのページ遷移についてポイントを説明していきます。

ここでは、次回に取り扱う状態管理も見据えてGetXというパッケージを使う方式を採用します。GetXは主にFlutterのコードをシンプルにすることを目的とした様々な機能を提供していて、非常に人気のあるパッケージです。

特にFlutterにおいて最も初心者殺しの状態管理の部分をうまく乗り越えていく上で、現状最適なものだと思います。ちなみにメッセージをぽんと表示するトーストなども標準的APIよりもGetXの方が圧倒的にシンプルです。

ページ遷移に関しては標準的なAPIとGetX方式でそう難易度に変わりはないかもしれませんが、記載がシンプルな方が何かと良いですしGetXで揃えていきます。





GetXの導入

まずはGetXパッケージを導入していきます。GetXの公式によるインストールガイドはこちらにあります。

pub.dev

本記事を書いている時点では下記のようになっています。バージョンの数値などが今後、変わっていくと思います。

f:id:linkedsort:20211017173257p:plain

まずは上記のページにある最新のバージョン番号の「get: ^4.3.8」(推奨バージョン番号はタイミングにより変わります)をpubspec.yamlのdependenciesのところに書き写すか、下記のコマンドを実行します:

flutter pub add get

Android Studioの場合は、下のスクリーンショットの左下のタブの3番目に「Terminal」というのがあって、これを押すとコマンドラインターミナルが出ますので、そこで「flutter pub add get」とタイプします。お好きなコマンドシェルでプロジェクトのルートフォルダのところに移動してこのコマンドを実行してもOKです。

f:id:linkedsort:20211017173136p:plain

そうすると、pubspec.yamlファイルにgetについての依存性の記載が追加されます。
上のサイトで最新のバージョンをチェックして、手でYAMLファイルに書いてしまっても大丈夫ですが、手書きするときは一応YAMLのフォーマットは確認しておいてくださいね。空白スペースに意味がある言語なので注意です。雑にやるとハマり兼ねません。
f:id:linkedsort:20211017173715p:plain

次にこのgetのファイルをプロジェクトに取り込む必要がありますので、コマンドラインで下記のように入力します:

flutter pub get

あるいはAndroid StudioなどのIDEがボタンを提供していたりしますので、これを押します。下のスクリーンショットであれば右上の「pub get」のボタンを押します。

f:id:linkedsort:20211017182034p:plain

これらの処理が無事に済んでいれば、プロジェクトのdartコード中で「import 'package:get/get.dart';」としてGetパッケージを使用可能になります。ここに波線が出てしまう場合には、どこかうまく行っていないことになります。


ここでハマりそうなポイントは:

  • pubspec.yamlの手書きで警告が出ている、うまく書けていない 
  • pubspec.yamlに依存性を書いただけで「flutter pub get」していない

あたりかと思います。importの時点でエラーが出ている場合は上記の説明を再確認してみてください。

ページ遷移

GetXが導入できたところで、ページ遷移のコードを見ていきます。

ここでは一気に3画面作っていてコード量が若干多めですが、よく見ると大したことを書いていません。コメント番号に対応する解説を下に載せますので、まずはゆっくりコードを見ていって下さい:

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

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    
    //(1) いつものMaterialAppにGetをつける
    return const GetMaterialApp(            
        debugShowCheckedModeBanner: false,
        //(2) ルートのパスを宣言
        initialRoute: '/',
        home:Home(),
    );
  }
}

//(3) ホームの画面
class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("Home Screen"),
        ),
        body:Column(
          children:[
            const Padding(
                padding: EdgeInsets.only(top:50)
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:[
                  ElevatedButton(
                    //(4)「to First」のボタンが押されたらFirstの画面に遷移
                    onPressed: ()=>Get.to(()=>const First()),
                    child: const Text("to First"),
                  ),
                  ElevatedButton(
                    //(5)「to Second」のボタンが押されたらSecondの画面に遷移
                    onPressed: ()=>Get.to(()=>const Second()),
                    child: const Text("to Second"),
                  ),
              ],
            )
        ]
      ),
    );
  }
}

//(6) Firstの画面
class First extends StatelessWidget {
  const First({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("First Screen"),
      ),
      body:Column(
          children:[
            const Padding(
                padding: EdgeInsets.only(top:50)
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:[
                ElevatedButton(
                  //(7)「戻る」遷移の挙動はこのように書きます
                  onPressed: ()=>Get.back(),
                  child: const Text("Back"),
                ),
              ],
            )
          ]
      ),
    );
  }
}

//(8) Secondの画面
class Second extends StatelessWidget {
  const Second({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Second Screen"),
      ),
      body:Column(
          children:[
            const Padding(
                padding: EdgeInsets.only(top:50)
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:[
                ElevatedButton(
                  //(9)「to Third」のボタンが押されたらThirdの画面に遷移
                  onPressed: ()=>Get.to(()=>const Third()),
                  child: const Text("to Third"),
                ),
                ElevatedButton(
                  //(10) ここではtoではなくoffを使った遷移(本文参照)
                  onPressed: ()=>Get.off(()=>const Third()),
                  child: const Text("off Third"),
                ),
                ElevatedButton(
                  //(11) こではtoやoffではなくoffAllを使った遷移(本文参照)
                  onPressed: ()=>Get.offAll(()=>const Third()),
                  child: const Text("off all Third"),
                ),
              ],
            )
          ]
      ),
    );
  }
}

//(12) Thirdの画面
class Third extends StatelessWidget {
  const Third({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Third Screen"),
      ),
      body:Column(
          children:[
            const Padding(
                padding: EdgeInsets.only(top:50)
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children:[
                ElevatedButton(
                  //(13) offAllでHomeに戻る遷移
                  onPressed: ()=>Get.offAll(()=>const Home()),
                  child: const Text("Go Home"),
                ),
              ],
            )
          ]
      ),
    );
  }
}

実行するとまずホームの画面が表示されます。ボタンが2つならんで設置されています:

f:id:linkedsort:20211017222822p:plain:w400

「to First」ボタンを押すと、下記の画面に遷移します。真ん中のボタンを押すか、左上の矢印を押すと元の画面に戻ります:

f:id:linkedsort:20211017222848p:plain:w400

ホームの「to Second」ボタンを押すと、下記の画面に遷移します。ここでは3つのボタンが有り、どのボタンを押しても同じ「Third Screen」の画面に遷移します。ただし遷移の仕方を変えてあり、左から「Get.to」「Get.off」「Get.offAll」のそれぞれのメソッドで遷移した場合の振る舞いの違いを見られるようにしてあります。

f:id:linkedsort:20211017222946p:plain:w400

「to Third」と「off Third」を押すと、下の画面に遷移します。見た目は同じですが、どちらのボタンを押してこの画面によって、左上の戻るボタンを押したときの挙動が変わってきます

f:id:linkedsort:20211017223045p:plain:w400

Second Screenから「off all third」ボタンを押すとこの画面。実装のWidgetとしても完全に上のThird Screenと同じ画面なのですが、左上の戻るボタンがないことに注意してください。

f:id:linkedsort:20211017223138p:plain:w400

なぜこのような違いがあるのか、その使い所などについては次のコードのポイントのなかで説明していきます。


コードのポイント

サンプルコードのコメントにつけた番号に対応して説明を述べていきます。

(1) GetXの開始点
 //(1) MaterialAppにGetをつける
    return const GetMaterialApp(            
        debugShowCheckedModeBanner: false,

MaterialAppウィジェットをルートに使うのが定番ですが、これにGetを付けた「GetMaterialApp」をルートのWidgetとして使います。

これはMaterialAppのすべての機能にGetXとして必要な機能、情報のパスを追加するものです。なのでMaterialAppを単にGetMaterialAppに変えても特に何か機能が削られるということはありません。

なお冒頭「import 'package:get/get.dart';」の宣言でgetパッケージをimportするのをお忘れなく。

(2) 開始点のウィジェットを指定
  //(2) ルートのパスを宣言
  initialRoute: '/',
  home:Home(),

ページ遷移にはウェブページのURLのように各ページにURLを割り付ける形と、直接その場でWidgetのクラスを呼び出す形があります。

ここではその場で呼び出す形式で進めます。開始点のWidgetクラスをhomeに指定します。

(3) 画面を構成するWidget
//(3) ホームの画面
class Home extends StatelessWidget {
  const Home({Key? key}) : super(key: key);

Homeの画面のWidgetを実装していきます。StatelessWidgetを継承します。GetXの世界ではStatefulWidgetを使いませんのでここは迷わずStatelessWidget一択という理解でOKです。

その下のコンストラクタはゴテゴテしていますが、これの必要性は以前に説明したとおりです。

(4) 「to First」ボタンの遷移
  ElevatedButton(
     //(4)「to First」のボタンが押されたらFirstの画面に遷移
     onPressed: ()=>Get.to(()=>const First()),
     child: const Text("to First"),
   ),

ここでボタンが押されたときに発動するonPressedに与えるコールバックで遷移のコードが出てきます。

見慣れないと複雑っぽい形をしていますが、onPressedが押されたら実行される関数が外側の「( )=>式 」の表記。これは関数の本体が1つの式で表されるときに適用できるアロー表記ですね。複数の命令文が必要な場合は「( ) { ここに命令文 }」の形を使います。

そのアロー表記で実行する中身が「Get.to( ()=>const First() )」です。更にここに関数のアロー表記がネストして書かれているので複雑ですが、要するに行き先のウィジェットのコンストラクタを指定しています。「to」がそこにいけ、ということですね。

なんだか複雑な形をしていますが最小限の表記にすると「Get.to(First())」でもOKだったりします。スッキリしますよね。ただこうすると、IDEが警告をだしてきて「Get.to( ()=>First() )」この形のほうがいいよ、と提示してきます。じゃあそうして、とボタンをポチっとして変形するとさらに、Firstにはconstを付けたほうがいいよ、と提示してきますので、これにも従ったのが最終形です。

慣れると気にならなくなるので最終の形を採用していますが、表記は短い方が良い、ということであれば無理に表記を増やさなくても良いと思います。


なぜGet.to(const First())ではなくGet.to( ()=> const First)がよいかというと、このコールバック形式で与えることによって、Firstのウィジェットが必要なくなったときに、メモリから完全に削除することができるから、というのが理由です。

つまり内部でメモリの参照カウントのような処理をウィジェットに対して行っていて、だれも参照しなくなったらガベージコレクションすることができる、というわけです。

使われなくなったウィジェットが画面遷移のたびにメモリ上に残って回収されなくなってしまっては困ったことに成りかねませんので、おすすめの形式には従っておいたほうが良さそうです。

(5) 「to Second」ボタンの遷移
   ElevatedButton(
      //(5)「to Second」のボタンが押されたらSecondの画面に遷移
      onPressed: ()=>Get.to(()=>const Second()),
      child: const Text("to Second"),
    ),

こちらも「to First」と同様、Get.toの遷移を採用しています。通常はこの遷移でOKです。

(6) Firstの画面構成
//(6) Firstの画面
class First extends StatelessWidget {
  const First({Key? key}) : super(key: key);

Firstの画面を構成するWidgetを定義しています。Homeと同様、StatelessWidgetを継承します。

(7) Firstの画面中央にある戻るボタン
   ElevatedButton(
      //(7)「戻る」遷移の挙動はこのように書きます
     onPressed: ()=>Get.back(),
     child: const Text("Back"),
   ),

「戻る」遷移はGet.back()という風に書きます。これはAppBarの左にある矢印の戻るボタンも同じ挙動です。

画面はスタック上に積み上がるようになっていて、Get.to(First())で遷移すると上に積み上がります。そしてGet.back()あるいは戻るボタンが押されるとトップの画面がなくなって、次に上に来るWidgetが画面上に表示されます:


f:id:linkedsort:20211109153652p:plain

(8) Secondの画面構成
//(8) Secondの画面
class Second extends StatelessWidget {
  const Second({Key? key}) : super(key: key);

次にSecondの画面構成の定義。Home、Firstと同様です。

(9)「to Third」ボタンの遷移
   ElevatedButton(
     //(9)「to Third」のボタンが押されたらThirdの画面に遷移
    onPressed: ()=>Get.to(()=>const Third()),
    child: const Text("to Third"),
  ),

比較のためのGet.to遷移。これはHome→First、Home→Secondと同じ挙動でありますので、下図のようにさらに上に積み上がります:


f:id:linkedsort:20211109164715p:plain

こうなるので、Get.toで遷移した「Thrid」から左上の戻るボタンで前の画面に戻ると「Second」に戻ることを確認してください。「Third」画面のボタンは「Go to Home」としていて強制的にHomeに戻しますので、ボタンの方はどの遷移から行ってもHomeに戻ります。

(10)「off Thrid」ボタンの遷移
   ElevatedButton(
     //(10) ここではtoではなくoffを使った遷移
    onPressed: ()=>Get.off(()=>const Third()),
    child: const Text("off Third"),
  ),

Get.offで遷移すると、現在の画面のスタックから取り除いた上で遷移します。下の図で違いを確認してください:


f:id:linkedsort:20211109164939p:plain

ですので先程とちがってoff遷移で「Third」画面に行ったときは、左上の戻るボタンで戻る先は「Home」になります。

(11)「off all Thrid」ボタンの遷移
   ElevatedButton(
     //(11) こではtoやoffではなくoffAllを使った遷移
    onPressed: ()=>Get.offAll(()=>const Third()),
    child: const Text("off all Third"),
  ),

ここまでくるとoffAllによる遷移の挙動は予測がつくかと思いますが、下記の図のとおりになります:


f:id:linkedsort:20211109165409p:plain

スタック上のWidgetは全て排除される形になります。Homeまでなくなっていることに注意してください。こうなるのでもう「戻る」場所はなくなっています。なのでこの遷移で「Third」画面に写った場合、バックする矢印が表示されなくなります。

(12)「Third」の画面構成
//(12) Thirdの画面
class Third extends StatelessWidget {
  const Third({Key? key}) : super(key: key);

「Third」の画面構成です。実装自体はほかの画面と代わりがありません。

(13)Homeに遷移するボタン
   ElevatedButton(
     //(13) offAllでHomeに戻る遷移
    onPressed: ()=>Get.offAll(()=>const Home()),
    child: const Text("Go Home"),
  ),

「Third」から「Home」に帰るためのボタンです。Secondのところで説明したとおり、スタックの状況が様々に変わりうるのでGet.backですと挙動が安定しません。またスタックがない状態でGet.backすることはできません。

なのでここではoffAllでHomeに返しています。これならどんな遷移からここに到達したとしてもこのボタンを押した後は「Home」が一つ乗っている状態になるからです。

また一つここで意識してきたいのは、戻るでスタックされているWidgetに戻る場合は、過去に作られたインスタンスを再利用しているのですが、破壊してコンストラクタから作る場合は、再度作成されています。見た目は変わらなくても内部の挙動の違いでパフォーマンスに影響が出る場合がありますので注意しましょう。





おわりに

今回はFlutterのページ遷移のうちGetXパッケージによる方法について解説しました。

ページ遷移として必須の要素に、遷移時のパラメタ渡しがあるのですが、少し長くなりましたので次の記事に続きを書来たいと思います。

GetXの導入から始まったので色々ありましたが、結局Get.toとその亜種ということを抑えておけばOKです。このあたりから、なぜか動かないという事象に悩まされて離脱されてしまう方が出てきてしまいますので長すぎですけれどもあまりコードを省略しない形で載せていきます。

まずはmain.dartにポンとコピペして、flutter pub add get、flutter pub getして動かしてみて下さい。一度動いたら、あれこれいじって動きを体感してみて下さい。

新しいライブラリやコーディングの様式を習得するときは、多少自分の趣味と違ったとしても動くコードをまず用意し、そこから自分のスタイルに合わせて変えていくのが定跡です。説明だけ聞いて自分で書き始めてハマると、挫折しかねませんからね。

f:id:linkedsort:20211109182711j:plain