Flutterチュートリアル:レイアウトの構築

Flutterの公式サイトは、とても丁寧で素晴らしい!けど、日本語はサポートされていない。ということで、公式サイトのマニュアルを日本語でまとめてみます。

公式サイトには、以下のレイアウト作成のチュートリアルがあります。

このチュートリアルで学べることは、以下の3つです。

このチュートリアルを終えると、以下のようなアプリケーションのレイアウトが完成します。

作成するアプリケーション

では、さっそくチュートリアルを開始しましょう。

Step.0 空のFlutterプロジェクトを作成する

最初のステップでは、以下の手順で新規にFlutterプロジェクトを作成します。

  1. VS Code で Cmd+Shift+P を押して「コマンドパレット」を開く
  2. 「Flutter: New Project」を入力して選ぶ
  3. 「Application」を選ぶ
  4. 「flutter_tutorial」という名前でプロジェクトを作成する

プロジェクトを作成したら、F5を押して実行します。すると、以下のようなデフォルトのアプリが起動します。

プロジェクトの作成と実行方法の詳細は、以下の記事が参考になります。

デフォルトのプロジェクトの main.dart は、余分な実装が含まれているので、中身を全部消します。以下のようにコードを変更します。

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 layout tutorial',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter layout tutorial'),
        ),
        body: const Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

コードがすっきりしましたね。実行すると、以下のようになります。

空の状態にする

ここで、MatrialApp の home部分を理解してみましょう。ここの部分が実際にレイアウトを構築している箇所になります。

Dart
home: Scaffold(
  appBar: AppBar(
    title: const Text('Flutter layout tutorial'),
  ),
  body: const Center(
    child: Text('Hello World'),
  ),
),

まず、Scaffold というウィジェットがあります。これは、レイアウトの土台に当たるウィジェットです。土台がないとレイアウトを配置できないので Scaffold が必要なんだ、ということを理解できていれば十分でしょう。

appBar が画面上の青色のタイトルバー部分です。title に Textウジェットを指定して、タイトル名を表示させています。

body がレイアウトのメイン部分になります。Centerウィジェットで画面中央に寄せるようにして、その中に Textウィジェットを置いています。これによって、画面中央に Hello World という文字が表示されています。

Step.1 レイアウトの要素を構造化する

さて、さっそくレイアウトを実装していきたいのですが、まずFlutterにおけるレイアウトの構造を理解する必要があります。このステップでは、ColumnとRowをつかって、全体の構成を考えます。

まず大きい要素で構造化します。このチュートリアルでは、画像・タイトル・ボタン・テキストの4つのColumn の要素があります。

4つのColumn要素

さらに、それぞれRowの要素を詳しくみてみます。「タイトル」の要素は、3つのRowと2つのColumnが組み合わさっています。

タイトルRowを構成する要素

同じく「ボタンRow」の要素は、3つのColumnと2つのRowが組み合わさっています。

ボタンRowを構成する要素

このように構造化しておくと、実装が楽になります。まとめると、以下のような全体構造になります。

全体の構造

それでは、次のステップからこの構造をプログラムで記述していきます。

Step.2 タイトル行の実装

Step.2 まず、タイトル行の左側の要素を実装します。MyApp クラスの build 関数の上に、以下のコードを書きます。

main.dart
// タイトル部分のウィジェット
Widget titleSection = Container(
  padding: const EdgeInsets.all(32), // 32pxのパディング
  child: Row(
    children: [
      // Expandedは、空白部分に要素をめいいっぱい広げる
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            // Containerはパディングを設定するために使う
            Container(
              padding: const EdgeInsets.only(bottom: 8),
              child: const Text(
                'Oeschinen Lake Campground',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                ),
              ),
            ),
            Text(
              'Kandersteg, Switzerland',
              style: TextStyle(
                color: Colors.grey[500],
              ),
            ),
          ],
        ),
      ),
      // アイコン
      Icon(
        Icons.star,
        color: Colors.red[500],
      ),
      const Text('41'),
    ],
  ),
);

この titleSection のウィジェットを以下のように MaterialApp の body に指定します。

main.dart
return MaterialApp(
      title: 'Flutter layout tutorial',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter layout tutorial'),
        ),
        // titleSection を指定する
        body: Column(
          children: [titleSection],
        ),
      ),
    );

これで、タイトル行の部分が表示されるようになります。

タイトル行が表示された

Step.3 ボタン部分の実装

ボタンの部分は、アイコンとテキストで構成される要素が3つ並んでいます。この3つの要素は、等間隔で横並びになってます。

同じ要素が3つあるので、3つ同じ実装をしても良いのですが、処理を共通化させるために MyApp クラス内に _buildButtonColumn という関数を用意します。

Dart
// ボタンのカラムを作成する関数(アンダーバーではじめると private関数になる)
// color : ボタンの色
// icon : アイコンの種類
// label : ボタンのテキスト
Column _buildButtonColumn(Color color, IconData icon, String label) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Container(
          margin: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }

タイトル部分と同じように、buttonSection を build関数内に用意します。

main.dart
// primary色を取得する
Color color = Theme.of(context).primaryColor;

// ボタンカラムを3つ横並びで用意する
Widget buttonSection = Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    _buildButtonColumn(color, Icons.call, 'CALL'),
    _buildButtonColumn(color, Icons.near_me, 'ROUTE'),
    _buildButtonColumn(color, Icons.share, 'SHARE'),
  ],
);

最後に、MaterialApp の body に buttonSection を指定します。

main.dart
return MaterialApp(
  title: 'Flutter layout tutorial',
  home: Scaffold(
    appBar: AppBar(
      title: const Text('Flutter layout tutorial'),
    ),
    body: Column(
      children: [titleSection, buttonSection],
    ),
  ),
);

これで、ボタン部分が実装完了です。

ボタン部分の実装

Step.4 テキスト部分の実装

テキスト部分も buttonSection と同様に入れます。上下左右に空白を入れたいので、Paddingウィジェットを用います。また、テキストの折り返す場所を単語の区切り目にするために、softWrap オプションを true に設定します。

main.dart
Widget textSection = const Padding(
  padding: EdgeInsets.all(32),
  child: Text(
    'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
    'Alps. Situated 1,578 meters above sea level, it is one of the '
    'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
    'half-hour walk through pastures and pine forest, leads you to the '
    'lake, which warms to 20 degrees Celsius in the summer. Activities '
    'enjoyed here include rowing, and riding the summer toboggan run.',
    softWrap: true,
  ),
);

MaterialApp の body に textSection を指定します。

main.dart
return MaterialApp(
  title: 'Flutter layout tutorial',
  home: Scaffold(
    appBar: AppBar(
      title: const Text('Flutter layout tutorial'),
    ),
    body: Column(
      children: [titleSection, buttonSection, textSection],
    ),
  ),
);

これでテキスト部分の実装が完了です。

テキスト部分の実装

Step.5 画像部分の実装

まず、画像ファイルをプロジェクトフォルダに追加します。わかりやすいように image フォルダを作成して、その中に 画像ファイルを入れます。

画像ファイルを追加する

あとは、プログラムから呼び出すだけ。としたいのですが、Flutterではアセットとして登録する必要があります。じゃっかん面倒ですね。以下のように pubspec.yaml ファイルを編集して、assets に先ほど追加した lake.jpg ファイルを指定します。このときに、yamlファイルではスペースが特別な意味を持つので、スペースも含めて設定するようにします。

pubspec.yaml
# The following section is specific to Flutter packages.
flutter:
  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  assets:
    - images/lake.jpg

最後に、dart プログラム側から呼び出します。これまでと同じように imageSection というWidgetを用意して、MaterialApp の body に追加します。

main.dart
 Widget imageSection = Image.asset(
  "images/lake.jpg",
  width: 600,
  height: 240,
  fit: BoxFit.cover,
);

return MaterialApp(
  title: 'Flutter layout tutorial',
  home: Scaffold(
    appBar: AppBar(
      title: const Text('Flutter layout tutorial'),
    ),
    body: Column(
      children: [imageSection, titleSection, buttonSection, textSection],
    ),
  ),
);

これで完成です。

Step.6 最後の調整

このままで完成としても良いのですが、テキストが長くなったときに現状の実装ではスクロールできません。スクロールできるように修正しておきましょう。スクロールさせるには body の Column ウィジェットではなくて、ListView ウィジェットを使うようにします。

Dart
return MaterialApp(
  title: 'Flutter layout tutorial',
  home: Scaffold(
    appBar: AppBar(
      title: const Text('Flutter layout tutorial'),
    ),
    body: ListView(
      children: [imageSection, titleSection, buttonSection, textSection],
    ),
  ),
);

スクロール操作が効くようになりました。

スクロール操作ができるように

まとめ

ああ