※自分の学習記録のメモとしてこの記事を書いています。
1日目:Flutter学習1日目 -公式を読む 1. インストール編-
2日目前編:Flutter学習2日目前編 -公式を読む 2. エディターの設定編-
2日目後編:Flutter学習2日目後編 -公式を読む 3. テストドライブ編-
︙
5日目:Flutter学習5日目 -公式を読む 6. Web開発者のためのFlutter-
6日目:Flutter学習6日目 -公式を読む 7. ウィジェットの紹介-
7日目:Flutter学習7日目 -公式を読む 8. レイアウトのチュートリアル-(イマココ)
今日はBuilding layouts
ページを読み進めて、レイアウトの仕組みや作り方について学んでいきます!
ステップ0: アプリのベースコードを作成
3日目に使用した「Hello World」を表示するためのコードをコピペします。
// 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'),
),
),
);
}
}
このコードを次のように修正します。
@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'),
ステップ1:レイアウトを図にする
最初のステップは、レイアウトを基本的な要素に分解することです。
- 行と列を識別します。
- レイアウトにはグリッドが含まれていますか?
- 重複する要素はありますか?
- UIにはタブが必要ですか?
- 整列、パディング、境界線が必要な部分に注目してください。
まず、大きな要素を確認します。この例では、4つの要素が列に配置されています:画像、2つの行、テキストのブロック。
次に、各行を図にします。最初の行は、タイトルセクションと呼ばれ、3つの子要素があります。最初の子である列には、2 行のテキストが含まれています。この最初の列は多くのスペースを必要とするので、拡張ウィジェットでラップする必要があります。
ボタンセクションと呼ばれる2列目にも3つの子があり、それぞれの子はアイコンとテキストを含む列です。
レイアウトが図解化されたら、それを実装するためにボトムアップのアプローチを取るのが最も簡単です。深く入れ子になったレイアウトコードの視覚的な混乱を最小限にするために、実装の一部を変数や関数に配置します。
ステップ2:タイトル行を実装する
はじめに、タイトル部分の左カラムをビルドします。MyApp クラスの build() メソッドの先頭に以下のコードを追加します。
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'),
],
),
);
そして下記のようにコードを修正します。
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,
+ ],
),
),
);
コードの解説💡
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
展開されたウィジェット内に列を配置すると、列は行内の残りの空きスペースをすべて使用するように引き伸ばされます。crossAxisAlignment プロパティを CrossAxisAlignment.start に設定すると、列は行の開始位置に配置されます。
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Container の中に Text の最初の行を入れると、パディングを追加することができます。列の2番目の子もテキストで、灰色で表示されます。
Icon(
Icons.star,
color: Colors.red[500],
),
Text('41'),
タイトル行の最後の2つの項目は、赤く塗られた星のアイコンと「41」というテキストです。行全体が Container 内にあり、各エッジに沿って32ピクセルのパディングが施されています。このようにタイトル部分をアプリ本体に追加します。
ステップ3:ボタン列を実装する
ボタンセクションには、テキストの列の上にアイコンを配置した同じレイアウトの3つの列があります。この列は等間隔に配置され、テキストとアイコンは原色で塗られています。
各カラムを作成するコードはほぼ同じなので、プライベート ヘルパー メソッド buildButtonColumn() を作成します。このメソッドは、色、 Icon 、 Text を受け取り、ウィジェットが指定した色で塗られたカラムを返します。
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 宣言のすぐ下に以下のコードを追加します。
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'),
],
),
);
本体にボタン部分を追加します。
body: Column(
children: [
titleSection,
+ buttonSection,
],
),
ステップ4:テキストセクションを実装する
テキストセクションを変数として定義します。テキストを Container に入れ、各エッジに沿ってパディングを追加します。 buttonSection 宣言のすぐ下に以下のコードを追加します。
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に設定すると、テキスト行は単語の境界で折り返す前に列の幅を埋め尽くすようになります。
テキストセクションを本文に追加します。
children: [
titleSection,
buttonSection,
+ textSection,
],
ステップ5:画像部分を実装する
これで4つの列要素のうち3つが完成し、画像だけが残りました。画像ファイルを追加します。
- プロジェクトのトップに画像ディレクトリを作成します。
- lake.jpg を追加します。
- pubspec.yamlファイルを更新して、assetsタグを含むようにします。これにより、画像をコードで利用できるようになります。
flutter:
uses-material-design: true
+ assets:
+ - images/lake.jpg
これでコードから画像を参照できるようになりました。
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
◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤
════════════════════════════════════════════════════════════════════════════════════════════════════
スクロール可能なコンテナを使用しろ、と言われたので次のステップでこのエラーを解消します。
ステップ6:ファイナルタッチ
この最後のステップでは、小さなデバイスでアプリを実行している場合、ListView はアプリ本体のスクロールをサポートしているため、すべての要素を Column ではなく ListView に配置します。
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,
これでエラーが出なくなりました。
これで完成です!🎉
最終的なソースコードはこちら。
// 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ページを読み進めてインタラクティブな機能の作り方について学んでいきます!🌼
そして次回で「公式を読む」編を終えた後、Udacity online Flutter Trainingをやっていく予定です!


