もふもふ技術部

IT技術系mofmofメディア

【Flutter 連載記事第2回】widgetを使った画面表示や、画面遷移をさせる

本記事について

本記事はシリーズ連載記事の第2回になります。

第1回では環境構築が完了しましたので、本記事ではその状態から引き続き進めていきます。

  1. flutterの環境構築
  2. widgetを使った画面表示や、画面遷移をさせる
  3. DBの設定やCRUDをする
  4. S3を使って画像アップロード・ダウンロードする
  5. TODOアプリを作成する

バージョン

  • OS: macOS Monterey 12.6
  • チップ: Apple silicon
  • Flutter SDK: flutter_macos_arm64_3.3.9-stable

デモコードの削除

実装を進める上で、一番大元になるのが lib/main.dart です。その他アプリで実装していくコードのファイルも lib ディレクトリ内に作成していくことになります。環境構築をした初期状態だと色々なコメントが記述されていますが、見づらいですので下記コードではコメント部分は削除しています。

lib/main.dart は下記のように void main() メソッドから MyApp を呼ぶ形になっています。

lib/main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

シミュレータを起動した際に表示されるデモページは class MyApp 内の

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

上記の箇所で設定されている MyHomePage が、 class MyHomePage 以下の箇所で実装されている内容で表示されていることが分かりますね。

flutter_add_widget1

今回は初期状態で実装されているページは使わないことにしようと思いますので、

class MyHomePage extends StatefulWidget {

上記の箇所から下のclass MyHomePage関連のコードは削除します。この状態だとエラーが出ていると思いますが、一旦そのままで進めます。

lib/main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

作成したファイルのインポートをする

main.dart に直接記述していくことも出来るのですが、最終的に画面遷移をさせようと思いますので分かりやすくするために main.dart とは別のファイルを作ってそれをimportする形にします。

今回は画像の一覧を表示させるページを作ろうと思いますので lib ディレクトリ内に新規に image_list.dart を作ります。

lib/image_list.dart

import 'package:flutter/material.dart';

class ImageList extends StatelessWidget {
  const ImageList({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: const Text('画像一覧ページです'),
    );
  }
}

画面上部のAppBarには「タイトル」、bodyには「画像一覧ページです」だけを表示させる簡単なページです。

このページを表示させるために、 lib/main.dart にこのファイルをインポートします。インポートするには lib/main.dart

import 'image_list.dart';

を追加します。パスは相対パスで記述出来るので、同じlibディレクトリ配下なので上記の形になります。そして、

- home: const MyHomePage(title: 'Flutter Demo Home Page'),
+ home: const ImageList(),

上記のようにhomeの引数でMyHomePageを渡していた箇所を、インポートしたImageList()を渡す形に変更します。変更後の lib/main.dart は下記になります。

lib/main.dart

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ImageList(),
    );
  }
}

この状態で flutter run を実行してシミュレータで表示させると、画面上部のAppBarには「タイトル」、bodyには「画像一覧ページです」が無事表示されました。

flutter_add_widget2

ImageListのページを表示出来ることが確認出来ましたので、現在テキストだけが表示させているところを画像表示させるように変更しましょう。

Widgetを使う

ではここからは具体的にWidgetを使って実装をしていきましょう。FlutterではUIを実装するにあたり、美しくかつ迅速に実装できるように最初から部品を用意してくれていて、その部品のことをWidgetと言います。

と言っても、今までの実装で既にWidgetは使ってきています。なぜならFlutterではUIに関わる部分はWidgetであるからです。例えば Text('画像一覧ページです') と実装してテキストを表示させているこの Text()Widgetですし、「タイトル」と文字を表示させている上部の AppBar()Widgetです。

Widgetの使い方はとても直感的で、あるWidgetの中に別のWidgetを入れることで親子構造を作ることが出来ます。例えば、

  • 画像を表示させるWidgetを、項目をグリッド表示させるWidgetの中に入れることで「画像をグリッド表示させる」という実装が出来る
  • 画像を表示させるWidgetを、ピンチイン/ピンチアウトで項目を拡大縮小表示出来るWidgetの中に入れることで「画像をピンチイン/ピンチアウトで拡大縮小表示出来るようにする」という実装が出来る

上記のような形で使うことができます。

ちなみにWidgetについては公式ドキュメントに Widget catalog がありますのでそちらでどういうWidgetがあるかを確認することが出来ますので、ざっと見てみるのも良いと思います。

それでは例で挙げた画像表示に関するWidgetの親子構造を使った上記の2点について、実際に実装して確認していきましょう。まずは

  • 画像を表示させるWidgetを、項目をグリッド表示させるWidgetの中に入れることで「画像をグリッド表示させる」という実装が出来る

こちらをやってみましょう。

最初に画像をWidgetで表示させようと思います。画像表示の方法はいくつかあるのですが、簡単にやれるのでここではインターネット上にアップロードされている画像を表示させる方法でやってみましょう。

例えばmofmof技術部で表示されているmofmofのロゴ画像のURLは https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png ですので、これを表示させてみましょう。

lib/image_list.dart の下記部分を変更してください。 Image Widgetを使います。やることは簡単で、Widgetが持っているconstructorメソッドに画像URLを引数として渡してあげるだけです。

lib/image_list.dart

- body: const Text('画像一覧ページです'),
+ body: Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),

flutter_add_widget3

こんな感じで画像が表示されました。では次にグリッド表示をさせてみましょう。グリッド表示をさせるには GridView Widgetを使います。

実装後の lib/image_list.dart と表示される画面は下記です。

lib/image_list.dart

import 'package:flutter/material.dart';

class ImageList extends StatelessWidget {
  ImageList({super.key});

  @override
  Widget build(BuildContext context) {

  final images = [
    Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
    Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
    Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
    Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
    Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
    Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
  ];  

    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: GridView.count(crossAxisCount: 2, children: images),
    );
  }
}

flutter_add_widget4

まず変数imagesを宣言して Image Widgetを6つ配列に格納します。その変数imagesを GridView.count の引数に渡してあげます。単体のWidgetを渡す場合はキーは child になるのですが、今回は配列で複数のWidgetを渡すのでキーが children になっています。 crossAxisCount のキーは画面上に何列で表示させるかを指定するもので、ここで2を指定しているので画面では2列で表示されます。これを3に変えると

flutter_add_widget5

上記のように3列で表示されます。分かりやすいですね。

GridView には GridView.count 以外にも GridView.extentGridView.builder などがあり、用途によって使い分けることが出来ますので、色々と試してみるのが良いと思います 。

では次に

  • 画像を表示させるWidgetを、ピンチイン/ピンチアウトで項目を拡大縮小表示出来るWidgetの中に入れることで「画像をピンチイン/ピンチアウトで拡大縮小表示出来るようにする」という実装が出来る

こちらをやってみましょう。

実はここまで画像を表示させてきた Image.network()Widgetだけでは、画面上でピンチイン/ピンチアウトをしても画像の拡大/縮小は出来ません。そのため、ピンチイン/ピンチアウトが有効になるWidgetの中に Image.network() を入れる、というやり方をすることになります。

lib/image_list.dart はグリッド表示をさせている状態になっているかと思いますが、一旦画像を1枚だけ表示させる状態に戻します。

シミュレータ上でmacのキーボードのoptionを押すと2つの丸が表示されます。この状態でマウスを左クリックしたままマウスを動かすとシミュレータ上でピンチイン/ピンチアウトの動作を行うことが出来ます。ただoptionを押した状態だけだと画面真ん中の位置からピンチイン/ピンチアウトをする形になるのですが、optionを押したままさらにshiftを押すとピンチイン/ピンチアウトを行う位置自体を変えられますので、表示されている画像上に丸が当たる状態にしてからピンチイン/ピンチアウトをしましょう。

やってみると、 Image.network()Widgetだけでは画面上でピンチイン/ピンチアウトをしても何も変化が無いことが分かると思います。

flutter_add_widget6

では、ピンチイン/ピンチアウトが出来るようにしましょう。そのためには、 InteractiveViewer Widgetを使います。

lib/image_list.dart

import 'package:flutter/material.dart';

class ImageList extends StatelessWidget {
  const ImageList({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: InteractiveViewer(
        child:
            Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
      ),
    );
  }
}

Image.network()WidgetInteractiveViewer の中に入れます。

flutter_add_widget7

このように、ピンチイン/ピンチアウトが出来るようになりました。

flutterには様々なWidgetがありますので、用途に合わせて上手く使っていきましょう。

画面遷移をさせる

環境構築後最初に表示されるデモページのように1枚の画面に機能が全て詰まっているアプリであれば画面遷移は不要ですが、複数のページで機能を分けるようなアプリの場合は画面遷移をさせたいことが多くあると思います。

画面遷移をさせる方法はいくつかあるのですが、複数のページを移動させる場合はRoutesを使う方法が個人的に使いやすかったですので、今回はRoutesを使う方法をご紹介します。

まず、現在の lib/main.dart を見てみましょう。

lib/main.dart

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

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ImageList(),
    );
  }
}

MaterialApp Widgetの引数のhomeでImageList()を渡すことで画面を表示させていますね。

ここを下記のように変更します。

- home: const ImageList(),
+ routes: {
+   '/': (context) => const ImageList(),
+ },

再度シミュレータを開いても、同じように画像が表示された画面が表示されることが分かると思います。

アプリ起動時に最初に表示される画面はroutesでは '/' で設定することが出来、ここで設定したものがアプリ起動時に表示されることになります。

先ほど画像をグリッド表示させる画面の実装と画像のピンチイン/ピンチアウトをさせる画面の実装を行いましたので、 lib ディレクトリの中にグリッド表示の画面を image_list.dart 、ピンチイン/ピンチアウトの画面を image_detail.dart として作成しましょう。 lib/main.dart のroutesは下記のように変更します。

routes: {
  '/': (context) => const ImageList(),
  '/image_detail': (context) => const ImageDetail(),
},

新しく作成した ImageDetail はimportされていないのでエラーが出ると思いますので、

import 'image_detail.dart';

も追記しておきましょう。

これで遷移前と遷移後の画面が用意出来ましたので、画面遷移の処理を実装しましょう。

画面遷移をさせるには、 Navigator Widgetを使います。 Navigator には色々なメソッドがあるのですが、今回のroutesを使った画面遷移の方法の場合は Navigator.pushNamed() を使います。

Navigator.pushNamed(context, '/image_detail',)

上記のように第1引数にcontext、第2引数にroutesの名前を渡してあげます。この context というのはclassを作成する際に記載している

class ImageList extends StatelessWidget {
  ImageList({super.key});

  @override
  Widget build(BuildContext context) {

ここで引数として渡されている BuildContext context のことです。contextについてはここでは細かいことは割愛させて頂きますが、このページを表示させるのに必要な情報が格納されているものだというイメージを持って頂けたら良いかと思います。

Navigator Widgetは画面遷移をさせる処理を行うだけのWidgetですので、これ自体には「画面をタップしたら」というような動作は設定出来ません。そこで、画面をタップした際の動作を設定するために GestureDetector Widgetを使います。

GestureDetector(
  onTap: () {
    // タップ時に行いたい処理
  },
  child: , // タップ範囲として設定するWidgetをchildに設定する
),

GestureDetector を上記のように設定することで、childとして設定したWidgetをタップした際に指定の処理を行うことが出来ます。

では実際に NavigatorGestureDetectorlib/image_list.dart 実装したのが下記です。

lib/image_list.dart

import 'package:flutter/material.dart';

class ImageList extends StatelessWidget {
  ImageList({super.key});

  @override
  Widget build(BuildContext context) {
    final images = [
      GestureDetector(
        onTap: () {
          Navigator.pushNamed(
            context,
            '/image_detail',
          );
        },
        child:
            Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
      ),
      Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
      Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
      Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
      Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
      Image.network('https://tech.mof-mof.co.jp/_nuxt/img/63792f2.png'),
    ];

    return Scaffold(
      appBar: AppBar(title: const Text('タイトル')),
      body: GridView.count(crossAxisCount: 2, children: images),
    );
  }
}

imagesで宣言している6つの画像のうち、最初の1つにだけ NavigatorGestureDetector を実装しています。シミュレータの動作はどうなるでしょうか。

flutter_add_widget8

最初の画像以外をタップしても何も変化はせず、最初の画像をタップした時のみ画面遷移が出来ていることが分かりますね。また、画面遷移後は左上の「<」をタップすることで元の画面に戻ることも出来ています。

画面左上の「<」で元の画面に戻る部分は AppBarWidgetが自動的に表示させているものになるのですが、アプリの仕様によっては画面遷移後に元の画面に戻したくない場合もあると思います。例えばログイン画面のような画面ですね。

そういう場合は、同じ Navigator Widgetの別のメソッドを使うことで一方通行の画面遷移を実現出来ます。

- Navigator.pushNamed(context, '/image_detail',)
+ Navigator.of(context).pushReplacementNamed('/image_detail',);

上記の形で、 Navigator.pushNamedNavigator.of(context).pushReplacementNamed に置き換えてみましょう。シミュレータの動作はどうなるでしょうか。

flutter_add_widget9

画面遷移後に左上の「<」が表示されなくなり、元の画面には戻れなくなりました。

これで画面遷移については一通り出来るようになりましたね。

最後に

今回はいくつかのWidgetの使い方の紹介と画面遷移の方法をご紹介しました。どのWidgetを使えば自分のやりたいことが実現できるか、というところについては都度調べながら実装していく必要はありますが、使うWidgetさえ決まればあとはパズルを組んでいくような感覚で実装していけるというのはflutterの面白みだなと思います。

参考

Flutter Doc JP GridView

Flutter Doc JP 画面遷移(Navigator)