本記事は下記リンクのFlutter公式ドキュメントを学習したメモです。

Layout in Flutter

Build a Flutter layout

開発途中のスクリーンショット以外の本記事で使用しているイメージは上記公式ドキュメントのイメージです。

概要

FlutterのほとんどはWidgetだ。Widgetを組み合わせてレイアウトを作る。

Icon Menu

上のイメージを見てみよう。3つのアイコンとアイコンの下にはラベルがついている。イメージを詳細なWidgetでみると次のイメージのように構成されている。

Icon Menu

Widgetの構造をツリーでみると次のように構成される。

flowchart TD
    w01((Container)) --> w02((Row))
    w02((Row)) --> w03((Column))
    w02((Row)) --> w04((Column))
    w02((Row)) --> w05((Column))
    w03((Column)) --> w06((Icon))
    w03((Column)) --> w07((Container))
    w07((Container)) --> w08((Text))
    w04((Column)) --> w09((Icon))
    w04((Column)) --> w10((Container))
    w10((Container)) --> w11((Text))
    w05((Column)) --> w12((Icon))
    w05((Column)) --> w13((Container))
    w13((Container)) --> w14((Text))
    style w01 fill:#f9f
    style w07 fill:#f9f
    style w10 fill:#f9f
    style w13 fill:#f9f

Containerは子WidgetをカスタマイズできるWidgetでパディング、マージン、境界線、背景色などを追加できる。

レイアウトを作ってみる。

The finished app

Photo by Dino Reichmuth on Unsplash. Text by Switzerland Tourism.

上のイメージは公式ドキュメントに載っているイメージで、イメージのレイアウトを構築してみよう。

上のイメージを大きく分けると次のような要素で分けられる。

The finished app

まずはレイアウトを構築するためにFlutterプロジェクトを作成して基本的な構造を作成しておこう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter layout demo'),
        ),
        body: const Center(
          child: Text('Hello World!'),
        ),
      ),
    );
  }
}

iOS simulator

タイトルはできた。次にイメージを入れてみよう。

まずはイメージを/assets/images/の下に入れてpubspec.yamlを修正する必要がある。

次のようになっている箇所を修正しよう。

1
2
3
4
5
6
7
8
flutter:

  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

下は修正後だ。

1
2
3
4
5
6
flutter:

  uses-material-design: true

  assets:
    - assets/images/

上で確認した要素は縦で並んでいたのでScffoldのbodyはColumnで良さそう。

イメージセクション

それぞれの大きな要素は繰り返し使う要素になる可能性も多いので別クラスで定義してみよう。

/libの下にwidgetsとフォルダを作成してimage_section.dartファイルを作成する。

StatelessWidgetを相続したImageSectionクラスを作成する。

コンストラクタのパラメータはイメージのパスだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:flutter/material.dart';

class ImageSection extends StatelessWidget {
  const ImageSection({super.key, required this.image});

  final String image;

  @override
  Widget build(BuildContext context) {
    return Image.asset(
      image,
      width: 600,
      height: 240,
      fit: BoxFit.cover,
    );
  }
}

main.dartに追加してみよう。

イメージのパスは注意しよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import 'package:firtst_sample/widgets/image_section.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 demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter layout demo'),
        ),
        body: const Column(
          children: [
            ImageSection(image: 'assets/images/lake.jpg'),
          ],
        ),
      ),
    );
  }
}

iOS simulator

タイトルセクション

次はタイトルだ。下の公式ドキュメントのイメージをみるとどのように構成されているかわかる。

Title Section

タイトルセクションも部品として作ってみよう。/lib/widgetsの下にtitle_section.dartファイルを作成する。

StatelessWidgetを相続するTitleSectionクラスを作成しよう。

コンストラクターのパラメータはタイトル(名前)と場所(ロケーション)をもらおう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import 'package:flutter/material.dart';

class TitleSection extends StatelessWidget {
  const TitleSection({
    super.key,
    required this.name,
    required this.location,
  });

  final String name;
  final String location;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32), // 外側の隙間
      // 横並びの3つの要素をRowで作る
      child: Row(
        children: [
          // Rowの残りのスペースを全て使う
          Expanded(
            // タイトルとロケーションが縦で並ぶのでColumn
            child: Column(
              // 左寄せ
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 下のみ隙間を作る
                Padding(
                  padding: const EdgeInsets.only(bottom: 8),
                  child: Text(
                    name,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  location,
                  style: TextStyle(
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),
          const Text('41'),
        ],
      ),
    );
  }
}

各Widgetはコメントを参照してもらいたい。次はmain.dartに追加だ。

1
2
3
4
5
6
7
8
9
        body: const Column(
          children: [
            ImageSection(image: 'assets/images/lake.jpg'),
            TitleSection(
              name: 'Oeschinen Lake Campground',
              location: 'Kandersteg, Switzerland',
            ),
          ],
        ),

Columnのところのみ載せているが、main.dartの全体は最後に載せる。

iOS simulator

ボタンセクション

次の公式ドキュメントのイメージをみよう。

Icon Button

ボタンが3つ、それぞれのボタンはアイコンとラベルでなっている。このボタンも部品化できるのではないか。

/lib/widgetsの下にbutton_with_text.dartファイルを作ってStatelessWidgetを相続するButtonWithTextクラスを作ろう。

コンストラクターのパラメータは色、アイコン、ラベルにしよう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import 'package:flutter/material.dart';

class ButtonWithText extends StatelessWidget {
  const ButtonWithText({
    super.key,
    required this.color,
    required this.icon,
    required this.label,
  });

  final Color color;
  final IconData icon;
  final String label;

  @override
  Widget build(BuildContext context) {
    // アイコンとラベルが縦並びなのでColumn
    return Column(
      // 空白を最小化
      mainAxisSize: MainAxisSize.min,
      // 縦の真ん中並び
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // アイコンの追加
        Icon(icon, color: color),
        // ラベルの上に隙間を作る
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
              fontSize: 12,
              fontWeight: FontWeight.w400,
              color: color,
            ),
          ),
        ),
      ],
    );
  }
}

上のコードはボタンなのでボタンを持っているボタンセクションを作ってボタンを追加しよう。

/lib/widgetsの下にbutton_section.dartファイルを作ってStatelessWidgetを相続するButtonSectionクラスを作る。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import 'package:firtst_sample/widgets/button_with_text.dart';
import 'package:flutter/material.dart';

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

  @override
  Widget build(BuildContext context) {
    // 色はプロジェクトのデフォルトを使う。
    final Color color = Theme.of(context).primaryColor;
    // このクラスに与えられたサイズを超えないようにSizedBox
    return SizedBox(
      // ボタン3つが横並びなのでRow
      child: Row(
        // 均等に横並び
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          // 電話ボタン
          ButtonWithText(
            color: color,
            icon: Icons.call,
            label: 'CALL',
          ),
          // 位置情報ボタン
          ButtonWithText(
            color: color,
            icon: Icons.near_me,
            label: 'ROUTE',
          ),
          // 共有ボタン
          ButtonWithText(
            color: color,
            icon: Icons.share,
            label: 'SHARE',
          ),
        ],
      ),
    );
  }
}

これをmain.dartに追加する。

1
2
3
4
5
6
7
8
9
10
        body: const Column(
          children: [
            ImageSection(image: 'assets/images/lake.jpg'),
            TitleSection(
              name: 'Oeschinen Lake Campground',
              location: 'Kandersteg, Switzerland',
            ),
            ButtonSection(),
          ],
        ),

iOS simulator

テキストセクション

じゃあ、最後だ!次は説明が表示されているテキストセクションを作ってみよう。

もちろんテキストセクションも部品化する。/lib/widgetsの下にtext_section.dartファイルを作ってStatelessWidgetを相続するTextSectionクラスを作る。

コンストラクターのパラメータは説明だ!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:flutter/material.dart';

class TextSection extends StatelessWidget {
  const TextSection({
    super.key,
    required this.descriotion,
  });

  final String descriotion;

  @override
  Widget build(BuildContext context) {
    // その側には空白を!
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Text(
        descriotion,
        // Widget内で自動改行する
        softWrap: true,
      ),
    );
  }
}

これもmain.dartに追加!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        body: const Column(
          children: [
            ImageSection(image: 'assets/images/lake.jpg'),
            TitleSection(
              name: 'Oeschinen Lake Campground',
              location: 'Kandersteg, Switzerland',
            ),
            ButtonSection(),
            TextSection(
              descriotion:
                  '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.',
            ),
          ],
        ),

iOS simulator

どうだ!終わった!

main.dartの全体を載せておこう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import 'package:firtst_sample/widgets/button_section.dart';
import 'package:firtst_sample/widgets/image_section.dart';
import 'package:firtst_sample/widgets/text_section.dart';
import 'package:firtst_sample/widgets/title_section.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 demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter layout demo'),
        ),
        body: const Column(
          children: [
            ImageSection(image: 'assets/images/lake.jpg'),
            TitleSection(
              name: 'Oeschinen Lake Campground',
              location: 'Kandersteg, Switzerland',
            ),
            ButtonSection(),
            TextSection(
              descriotion:
                  '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.',
            ),
          ],
        ),
      ),
    );
  }
}

最後に

本記事はFlutter公式ドキュメントのLayoutの基本の箇所を学習してみたものである。

しばらくはLayoutセクションを続けて配信する。

コメントする