その他
    ホーム 職種別 エンジニア Flutterによるアプリ開発 <ウィジェットレイアウト編>
    Flutterによるアプリ開発 <ウィジェットレイアウト編>
     

    Flutterによるアプリ開発 <ウィジェットレイアウト編>

    ゲームデザイン部エンジニア中村です。リモートワーク中は昼飯自炊勢です。

    前回はFlutterによるアプリ開発<導入編>をお送りしました。今回はFlutterのウィジェットを実際に並べていき、アプリらしい画面を整えていきましょう。

    そのためには、Flutterの深層までの理解は必要ありません。この記事ではレイアウトを作成するための知識だけを提供します。一部のソースコードについてはおまじないと思っていただければ問題ありません。エンジニア初心者やデザイナの方でもコピー&ペーストするだけでレイアウトが作れるようにしてきます。

    ウィジェットレイアウトの基本

    初期生成ファイルの理解

    まずはプロジェクト生成時に自動で作られたファイルの内容をおおよそ理解しましょう。

    ここでは自動生成のコメントをすべて削除した状態にしています。また、ここで編集するファイルはlib/main.dartのみです。その中でどのようにウィジェットを指定し、テキストやボタンを表示しているのか見ていきましょう。

    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatelessWidget {
      @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'),
        );
      }
    }

    このブロックでまず注目すべきはprimarySwatch: Colors.blueです。これはFlutterに事前に用意されたテーマカラーを指定することでヘッダやボタンなどの基本色を指定することができます。Colors.greenColors.cyanなどが用意されています。

    また、MyHomePage(title: 'Flutter Demo Home Page')の箇所ではヘッダに表示するタイトルを指定しています。ここも自由に指定すると良いでしょう。

    上記2箇所について、今回は以下のように編集しました。

    ~~
    primarySwatch: Colors.green,
    ~~
    home: MyHomePage(title: 'Dashboard')

    続いて、ウィジェットのレイアウトをしている箇所です。

    Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              '右下のボタンをクリック',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );

    Scaffoldは工事などに利用する「足場」という意味で、足場を組んでから内部の部品を組み立てていくというイメージです。Flutter/Dartによるコードの構造として、ウィジェット名()としたときの()の中に更にウィジェットが記述される形になっています。

    Centerについてはchild:に対してウィジェットを指定するため、これがそのまま親子関係になります。Columnは複数の子要素を持つことからchildren:に対してウィジェットの配列を指定します。

    FloatingActionButtonは右下に表示されているボタンです。これはScafoldに対して1つだけ指定することができるウィジェットであり、ColumnCenterなどの子要素になることはありません。

    ハンズオンでレイアウトする

    CenterとColumnの基本レイアウト構造

    それではソースコードを簡単に編集していきましょう。まずは見やすくするためにScafoldの内部をCenterとその中のTextだけにします。

    Scaffold(
      body: Center(
        child: Text(
          'これはテストです',
        ),
      ),
    );

    ヘッダもフローティングボタンもなくなり、中央にテキストが表示されるだけとなりました。これにより、Center子要素を中央に表示するウィジェットであるとわかると思います。

    続いて、CenterColumnに書き換え、Textを3つに増やしましょう。このとき、Columnchildではなくchildrenの指定となります。

    Scaffold(
      body: Column(
            children: <Widget> [
              Text('これはテストです'),
              Text('これはテストです'),
              Text('これはテストです'),
            ]
          )
      ),
    );

    左上にテキストが3つ並ぶ形となりました。Columnchildrenに指定された子要素を縦に並べるウィジェットです。現状では中央に寄せる設定をしていないためこのような表示になります。

    ここでColumn内部を以下のように書き換えましょう。重要なのはmainAxisAlignmentcrossAxisAlignmentです。

    Scaffold(
      body: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: <Widget> [
              Text('これはテストです'),
              Text('これはテストですテストです'),
              Text('これはテストですテストですテストです'),
            ]
          )
      ),
    );

    テキストが中央に表示され、長さの違うテキストは右寄せになっています。これがmainAxisAlignmentcrossAxisAlignmentの効果です。以下の画像にその効果を図示します。

    緑の矩形がColumn自身を表します。縦方向には親要素を満たすようになりますが、横方向には子要素の最大幅に合わせます。

    この中でMainAxisとは、Columnの場合子要素を積み重ねる縦方向の意味になります。つまり、MainAxisAlignment.centerとすることで青い矩形で示した子要素のまとまりを縦方向に対して中央に配置することができます。

    そして、CrossAxisはそれに直交する方向となるため赤い矩形で示した子要素自体がそれぞれ横方向に整列することになります。今回はそれぞれのテキストの長さが異なり、CrossAxisAlignment.endとしたことから最も横幅が長い子要素に合わせて右揃えとなっています。同様に、startとすれば左揃え、centerとすれば中央揃えを表現できます。

    Columnは縦に積み重ねるウィジェットですが、横に並べるウィジェットとしてRowも存在します。Rowの場合には要素を横並びにするため、MainAxisは横方向となり、CrossAxisは縦方向となります。ColumnRowはレイアウトを作成する上で頻出ウィジェットですのでMainAxisCrossAxisを覚えておきましょう。

    リストを表示する

    SNSやショッピングアプリなどではリストビューがよく使われていると思います。Flutterではそういったリストを表示するためのウィジェットが用意されています。以下のようにコードを書き換えてみましょう。

    Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListView(
          children: [
            ListTile(
              leading: Icon(Icons.person),
              title: Text("人物アイコン"),
              onTap: () {},
            ),
            ListTile(
              leading: Icon(Icons.mail),
              title: Text("メールアイコン"),
              onTap: () {},
            ),
            ListTile(
              leading: Icon(Icons.map),
              title: Text("地図アイコン"),
              onTap: () {},
            )
          ],
        ),
      )
    );

    ListViewウィジェットのchildrenに対して子要素を指定するとそれだけでリストを表示することができます。このときListTileウィジェットを利用すると、アイコンとテキストの組み合わせというよく見るリストを作ることができます。onTapを指定することでそれぞれのリスト項目をタッチした際の処理も追加できます。

    また、ListViewの親要素となっているPaddingウィジェットはCSSなどでも見られるpaddingを意味し、レイアウトでは頻繁に使われるため覚えておきましょう。

    続いて、同様のリスト表示であるグリッドビューを試します。様々なレイアウト方式で、コレクションビューやタイルビューと呼ばれるリスト表示です。以下のようにコードを編集しましょう。

    Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: GridView.count(
          crossAxisCount: 2,
          crossAxisSpacing: 4,
          mainAxisSpacing: 4,
          children: [
            ListTile(
              leading: Icon(Icons.person),
              title: Text("人物アイコン"),
              onTap: () {},
            ),
            ListTile(
              leading: Icon(Icons.mail),
              title: Text("メールアイコン"),
              onTap: () {},
            ),
            ListTile(
              leading: Icon(Icons.map),
              title: Text("地図アイコン"),
              onTap: () {},
            )
          ],
        ),
      )
    );

    GridView.countでグリッドを作成し、childrenに子要素を指定します。今回はListViewと同じListTileを指定しているのでアイコンとタイトルの表示のみです。それでもグリッドビューらしく自動で矩形表示が行われます。GridView.countとしてウィジェットを指定すると、crossAxisCountに横方向に要素をいくつ並べるかを指定します。タブレットなどの大きい端末では大きい数値にすると見やすくなるでしょう。

    データからリストを作る

    ショッピングアプリは表示する商品情報を常にサーバから取得しなければなりません。そのためには、取得したデータからレイアウトを表示すべきです。今回の例ではリストに表示する子要素をサーバから取得し、ListTileのタイトルに反映されなければなりません。これを実現するために、まずは用意されたデータからListTileを生成してみましょう。

    // 最上部2行目付近
    const datas = [
      {'icon': Icons.person,'title': "人物アイコン",},
      {'icon': Icons.mail,'title': "メールアイコン",},
      {'icon': Icons.map,'title': "地図アイコン",},
      {'icon': Icons.map,'title': "地図アイコン",},
      {'icon': Icons.map,'title': "地図アイコン",},
      {'icon': Icons.play_arrow,'title': "再生ボタン",},
      {'icon': Icons.photo_sharp,'title': "写真アイコン",},
    ];
    ~~~~
    Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: ListView(
          children: datas.map[1]e) => 
            ListTile(leading: Icon(e['icon']),
              title: Text(e['title']),
              onTap: () {},
            )
          ).toList(),
        ),
      … Continue reading.toList(),
              );
            }
          },
        ),
      )
    );

    上記画像のように天気の表示ができたでしょうか。HTTP通信などで時間がかかる処理を待つ場合にはFutureBuilderを使用します。これはfutureに指定した処理が終わるのを待ってウィジェットを表示することができます。

    builderの中でif(!snapshot.hasData)という書き方をしていますが、これはFutureBuilderが待っている間、データの取得ができていないため代わりとなるウィジェットを表示しています。今回はCircularProgressIndicatorを指定し円形の読込中マークを表示しました。データの読み込みが完了するとその結果がsnapshot.dataに渡されますので、API結果のJSON文字列をデコードし、得られたデータからListTileウィジェットを作成しています。せっかくなので天気マークはAPIで得られる画像を利用しています。

    まとめ

    今回はFlutterの基本的なレイアウト構造であるCenter/Columnと、様々なアプリでよく利用されるListViewGridViewについて説明しました。さらにHTTP通信結果をレイアウトに反映する方法について記述しましたので、これだけでも簡単なSNSやチャットツールなどを作ることができるかと思います。

    Flutterにはまだまだたくさんのウィジェットがありますので、やってみたいレイアウトなどがあれば公式ドキュメントのウィジェット一覧が非常に参考になるのでご一読ください。

    次回はFlutterとFirebaseの連携についてまとめたいと思います。

    References

    References
    1 e) => ListTile(leading: Icon(e['icon']), title: Text(e['title']), onTap: () {}, ) ).toList(), ), ) );

    https://spirits.appirits.com/wp-content/uploads/2021/01/Simulator-Screen-Shot-iPhone-11-Pro-Max-2021-01-25-at-17.11.53-139x300.png 139w, https://spirits.appirits.com/wp-content/uploads/2021/01/Simulator-Screen-Shot-iPhone-11-Pro-Max-2021-01-25-at-17.11.53-194x420.png 194w" sizes="(max-width: 240px) 100vw, 240px" />

    ソースコード上部のimport文が記載された下にデータを作成します。そしてこのデータを使うようにGridViewの中を書き換えました。GridViewchildrenに対しては以下のような処理順を行っています。

    1. datasに対してmapすることで、含まれるデータ一覧の全てに対して共通の処理を行う
    2. 各データに対してListTileを作成し、leadingtitleにそれぞれデータを指定する
    3. 最後にmapしたデータをリストに変換する。

    このようにすることで与えられたデータから一覧を表示することができます。

    それでは実際にサーバからデータを取得し表示してみましょう。今回は天気予報APIをお借りして東京都の天気を取得し一覧表示します。

    まずはHTTP通信できるようにしなければなりません。以下のようにpubspec.yamlに1行を追加して保存しましょう。Flutterでは追加で利用するプラグインなどをこのファイルに記述します。

    dependencies:
      flutter:
        sdk: flutter
      http: ^0.12.2    // ← 追加

    これでhttpプラグインが利用できるようになったので、main.dart側でHTTP通信を利用するための宣言をしましょう。同時にJSON文字列をデコードするためのプラグインもimportします。

    import 'package:flutter/material.dart';
    import 'package:http/http.dart' as http;   // ←追加
    import 'dart:convert'; // ←追加

    プラグインを利用するために書き換える箇所は以上です。それでは天気予報APIから東京都の天気を取得して表示してみます。以下のような書き換えを行います。

    Scaffold(
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: FutureBuilder(
          future: http.get("https://weather.tsukumijima.net/api/forecast?city=130010"),
          builder: (context, snapshot) {
            if(!snapshot.hasData) {
              return CircularProgressIndicator();
            } else {
              var list = json.decode(snapshot.data.body) as Map<String, dynamic>;
              return ListView(
                children: (list['forecasts'] as List<dynamic>).map((e)=>ListTile(
                  leading: Image.network(e['image']['url']),
                  title: Text("${e['dateLabel']} - ${e['telop']}"),
                  subtitle: Text("${e['date']}"),