Flutter学習7日目 -公式を読む 8. レイアウトのチュートリアル-

**※自分の学習記録のメモとしてこの記事を書いています。**

1日目：[Flutter学習1日目 -公式を読む 1. インストール編-](https://qiita.com/umiremix/items/940f38e03be4b71dc33d)
2日目前編：[Flutter学習2日目前編 -公式を読む 2. エディターの設定編-](https://qiita.com/umiremix/items/0ff6600a6a9f996a3a74)
2日目後編：[Flutter学習2日目後編 -公式を読む 3. テストドライブ編-](https://qiita.com/umiremix/items/f1a84f96199054fbebf2)
︙
5日目：[Flutter学習5日目 -公式を読む 6. Web開発者のためのFlutter-](https://qiita.com/umiremix/items/ed9e61cd4e4c17ed1bdf)
6日目：[Flutter学習6日目 -公式を読む 7. ウィジェットの紹介-](https://qiita.com/umiremix/items/3844119d4510884bf3b1)
7日目：Flutter学習7日目 -公式を読む 8. レイアウトのチュートリアル-（イマココ）

今日は[Building layouts
](https://flutter.dev/docs/development/ui/layout/tutorial)ページを読み進めて、レイアウトの仕組みや作り方について学んでいきます！

## ステップ0: アプリのベースコードを作成

[3日目](https://qiita.com/umiremix/items/796cc8a7520629cae3b1)に使用した「Hello World」を表示するためのコードをコピペします。

```dart
// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Welcome to Flutter',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Welcome to Flutter'),
        ),
        body: Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}
```

このコードを次のように修正します。

```diff
@override            
Widget build(BuildContext context) {            
  return MaterialApp(            
- title: 'Welcome to Flutter',            
+ title: 'Flutter layout demo',            
  home: Scaffold(            
    appBar: AppBar(            
-   title: Text('Welcome to Flutter'),            
+   title: Text('Flutter layout demo'),            
  ),            
  body: Center(            
  child: Text('Hello World'),     
```

<img width="529" alt="スクリーンショット 2020-08-10 14.26.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/f51eb397-530a-8b05-ced0-69600031fe06.png">

## ステップ1：レイアウトを図にする

最初のステップは、レイアウトを基本的な要素に分解することです。

- 行と列を識別します。
- レイアウトにはグリッドが含まれていますか？
- 重複する要素はありますか？
- UIにはタブが必要ですか？
- 整列、パディング、境界線が必要な部分に注目してください。

まず、大きな要素を確認します。この例では、4つの要素が列に配置されています：画像、2つの行、テキストのブロック。

![スクリーンショット 2020-08-10 14.29.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/5e9e1895-286c-cb45-9870-357f72c76d20.png)

次に、各行を図にします。最初の行は、タイトルセクションと呼ばれ、3つの子要素があります。最初の子である列には、2 行のテキストが含まれています。この最初の列は多くのスペースを必要とするので、拡張ウィジェットでラップする必要があります。

![スクリーンショット 2020-08-10 14.36.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/38de6780-9e3a-b1e4-dee2-271826b4e18f.png)

ボタンセクションと呼ばれる2列目にも3つの子があり、それぞれの子はアイコンとテキストを含む列です。

![スクリーンショット 2020-08-10 14.36.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/b2479aae-f1d0-b443-4583-ec8a3e338a3b.png)

レイアウトが図解化されたら、それを実装するためにボトムアップのアプローチを取るのが最も簡単です。深く入れ子になったレイアウトコードの視覚的な混乱を最小限にするために、実装の一部を変数や関数に配置します。

## ステップ2：タイトル行を実装する

はじめに、タイトル部分の左カラムをビルドします。`MyApp` クラスの `build()` メソッドの先頭に以下のコードを追加します。

```dart

Widget titleSection = Container(
  padding: const EdgeInsets.all(32),
  child: Row(
    children: [
      Expanded(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Container(
              padding: const EdgeInsets.only(bottom: 8),
              child: 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],
      ),
      Text('41'),
    ],
  ),
);
```

そして下記のようにコードを修正します。

```dart
return MaterialApp(            
  title: 'Flutter layout demo',            
  home: Scaffold(            
    appBar: AppBar(            
    title: Text('Flutter layout demo'),            
  ),            
- body: Center(            
-   child: Text('Hello World'),            
+   body: Column(            
+     children: [            
+       titleSection,            
+     ],            
    ),            
  ),            
);
```

<img width="529" alt="スクリーンショット 2020-08-10 14.43.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/46855abe-e385-359c-658f-1f35fdcfa3fb.png">

### コードの解説💡

```dart

child: Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
```

展開されたウィジェット内に列を配置すると、列は行内の残りの空きスペースをすべて使用するように引き伸ばされます。`crossAxisAlignment` プロパティを `CrossAxisAlignment.start` に設定すると、列は行の開始位置に配置されます。

```dart

Container(
  padding: const EdgeInsets.only(bottom: 8),
  child: Text(
    'Oeschinen Lake Campground',
    style: TextStyle(
      fontWeight: FontWeight.bold,
    ),
  ),
),
```

`Container` の中に `Text` の最初の行を入れると、パディングを追加することができます。列の2番目の子もテキストで、灰色で表示されます。

```dart

Icon(
  Icons.star,
  color: Colors.red[500],
),
Text('41'),
```

タイトル行の最後の2つの項目は、赤く塗られた星のアイコンと「41」というテキストです。行全体が `Container` 内にあり、各エッジに沿って32ピクセルのパディングが施されています。このようにタイトル部分をアプリ本体に追加します。

## ステップ3：ボタン列を実装する

ボタンセクションには、テキストの列の上にアイコンを配置した同じレイアウトの3つの列があります。この列は等間隔に配置され、テキストとアイコンは原色で塗られています。

各カラムを作成するコードはほぼ同じなので、プライベート ヘルパー メソッド `buildButtonColumn()` を作成します。このメソッドは、色、 `Icon` 、 `Text` を受け取り、ウィジェットが指定した色で塗られたカラムを返します。

```dart
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // ···
  }

  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,
            ),
          ),
        ),
      ],
    );
  }
}
```

この関数は、アイコンをカラムに直接追加します。テキストは、上部のみの余白を持つコンテナ内にあり、テキストとアイコンを分離しています。

関数を呼び出して、その列に固有の色、アイコン、テキストを渡して、これらの列を含む行を作成します。`MainAxisAlignment.spaceEvenly` を使用して主軸に沿って列を整列させ、各列の前後、間、および後のフリースペースを均等に配置します。`build()` メソッド内の `titleSection` 宣言のすぐ下に以下のコードを追加します。

```dart

Color color = Theme.of(context).primaryColor;

Widget buttonSection = Container(
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      _buildButtonColumn(color, Icons.call, 'CALL'),
      _buildButtonColumn(color, Icons.near_me, 'ROUTE'),
      _buildButtonColumn(color, Icons.share, 'SHARE'),
    ],
  ),
);
```

本体にボタン部分を追加します。

```diff
body: Column(            
  children: [            
    titleSection,            
+   buttonSection,            
  ],            
),
```

<img width="529" alt="スクリーンショット 2020-08-10 15.34.12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/746973ee-fdc1-93db-2942-0b791eac18fa.png">

## ステップ4：テキストセクションを実装する

テキストセクションを変数として定義します。テキストを `Container` に入れ、各エッジに沿ってパディングを追加します。 `buttonSection` 宣言のすぐ下に以下のコードを追加します。

```dart

Widget textSection = Container(
  padding: const 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,
  ),
);
```
`softwrap` をtrueに設定すると、テキスト行は単語の境界で折り返す前に列の幅を埋め尽くすようになります。

テキストセクションを本文に追加します。

```diff
children: [            
  titleSection,            
  buttonSection,            
+ textSection,            
],
```

## ステップ５：画像部分を実装する

これで4つの列要素のうち3つが完成し、画像だけが残りました。画像ファイルを追加します。

- プロジェクトのトップに画像ディレクトリを作成します。
- [lake.jpg](https://github.com/flutter/website/tree/master/examples/layout/lakes/step6/images) を追加します。
- pubspec.yamlファイルを更新して、assetsタグを含むようにします。これにより、画像をコードで利用できるようになります。

```
flutter:            
  uses-material-design: true            
+ assets:            
+    - images/lake.jpg
```

これでコードから画像を参照できるようになりました。

```diff
children: [            
+ Image.asset(            
+   'images/lake.jpg',            
+   width: 600,            
+   height: 240,            
+   fit: BoxFit.cover,            
+ ),            
  titleSection,            
  buttonSection,            
  textSection,
],
```

`BoxFit.cover` はフレームワークに、画像をできるだけ小さくして、レンダリングボックス全体をカバーするように指示します。

しかし今の状態では下記のようなエラーが返ってきます。

```
══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown during layout:
A RenderFlex overflowed by 18 pixels on the bottom.
The relevant error-causing widget was:
  Column 
lib/main.dart:78
The overflowing RenderFlex has an orientation of Axis.vertical.
The edge of the RenderFlex that is overflowing has been marked in the rendering with a yellow and
black striped pattern. This is usually caused by the contents being too big for the RenderFlex.
Consider applying a flex factor (e.g. using an Expanded widget) to force the children of the
RenderFlex to fit within the available space instead of being sized to their natural size.
This is considered an error condition because it indicates that there is content that cannot be
seen. If the content is legitimately bigger than the available space, consider clipping it with a
ClipRect widget before putting it in the flex, or using a scrollable container rather than a Flex,
like a ListView.
The specific RenderFlex in question is: RenderFlex#1c49c relayoutBoundary=up1 OVERFLOWING:
  creator: Column ← _BodyBuilder ← MediaQuery ← LayoutId-[<_ScaffoldSlot.body>] ←
    CustomMultiChildLayout ← AnimatedBuilder ← DefaultTextStyle ← AnimatedDefaultTextStyle ←
    _InkFeatures-[GlobalKey#57837 ink renderer] ← NotificationListener<LayoutChangedNotification> ←
    PhysicalModel ← AnimatedPhysicalModel ← ⋯
  parentData: offset=Offset(0.0, 76.0); id=_ScaffoldSlot.body (can use size)
  constraints: BoxConstraints(0.0<=w<=375.0, 0.0<=h<=591.0)
  size: Size(375.0, 591.0)
  direction: vertical
  mainAxisAlignment: start
  mainAxisSize: max
  crossAxisAlignment: center
  verticalDirection: down
◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤
════════════════════════════════════════════════════════════════════════════════════════════════════
```

<img width="529" alt="スクリーンショット 2020-08-10 16.01.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/eb830a4f-656d-c658-dcad-d9128e147377.png">

スクロール可能なコンテナを使用しろ、と言われたので次のステップでこのエラーを解消します。


## ステップ6：ファイナルタッチ

この最後のステップでは、小さなデバイスでアプリを実行している場合、`ListView` はアプリ本体のスクロールをサポートしているため、すべての要素を `Column` ではなく `ListView` に配置します。

```diff
return MaterialApp(            
  title: 'Flutter layout demo',            
  home: Scaffold(            
    appBar: AppBar(            
      title: Text('Flutter layout demo'),            
    ),            
-   body: Column(            
+   body: ListView(            
      children: [            
        Image.asset(            
          'images/lake.jpg',            
           width: 600,            
           height: 240,            
           fit: BoxFit.cover,
```

これでエラーが出なくなりました。

<img width="529" alt="スクリーンショット 2020-08-10 16.07.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62182/5e5169d0-a30d-044c-c86f-2b0d2344f3be.png">

これで完成です！🎉

最終的なソースコードはこちら。

```dart:main.dart

// Copyright 2018 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget titleSection = Container(
      padding: const EdgeInsets.all(32),
      child: Row(
        children: [
          Expanded(
            /*1*/
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                /*2*/
                Container(
                  padding: const EdgeInsets.only(bottom: 8),
                  child: Text(
                    'Oeschinen Lake Campground',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                Text(
                  'Kandersteg, Switzerland',
                  style: TextStyle(
                    color: Colors.grey[500],
                  ),
                ),
              ],
            ),
          ),
          /*3*/
          Icon(
            Icons.star,
            color: Colors.red[500],
          ),
          Text('41'),
        ],
      ),
    );
    Color color = Theme.of(context).primaryColor;
    Widget buttonSection = Container(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          _buildButtonColumn(color, Icons.call, 'CALL'),
          _buildButtonColumn(color, Icons.near_me, 'ROUTE'),
          _buildButtonColumn(color, Icons.share, 'SHARE'),
        ],
      ),
    );
    Widget textSection = Container(
      padding: const 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,
      ),
    );
    return MaterialApp(
      title: 'Flutter layout demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter layout demo'),
        ),
        body: ListView(
          children: [
            Image.asset(            
              'images/lake.jpg',            
              width: 600,            
              height: 240,            
              fit: BoxFit.cover,            
            ),
            titleSection,
            buttonSection,
            textSection
          ],
        ),
      ),
    );
  }
  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,
            ),
          ),
        ),
      ],
    );
  }
}
```

# 感想

今回はレイアウトについて学びました！分解して考えたりするのは普段書いているHTML、CSSと変わらないのでスムーズに読み解くことができました。😊
次回は[Add interactivity tutorial](https://flutter.dev/docs/development/ui/interactive)ページを読み進めてインタラクティブな機能の作り方について学んでいきます！🌼
そして次回で「公式を読む」編を終えた後、[Udacity online Flutter Training](https://www.udacity.com/course/build-native-mobile-apps-with-flutter--ud905)をやっていく予定です！