元記事
https://docs.flutter.dev/get-started/fundamentals/layout
意訳しています.
はじめに
FlutterがUIツールキットを与えられたら,Flutterウィジェットを使ってレイアウトを作ることに時間を費やすだろう.
このセクションでは,いくつかの一般的なレイアウトウィジェットを使ってレイアウトの組み立て方を学ぶ.
Flutterがどのようにレイアウトを作るのか理解するために,Flutter DevToolsを使うだろう.
最後に,Flutterの最も一般的なレイアウトエラーの1つである,恐ろしい「unbounded constraints」エラーに遭遇し,デバッグするだろう.
Flutterのレイアウトを理解する
Flutterレイアウト機構の中心部分はウィジェットである.
Flutterにおいて,たとえレイアウトモデルがウィジェットであったとしても,ほとんどすべてのものはウィジェットである.
Flutterアプリで見れるImage
,Icon
,Text
はすべてウィジェットである.
見ることもできないものもウィジェットである.
例えば,Rows
,Columns
,grids
は見えるウィジェットを配置し,制約し,整列させる.
複雑なウィジェットを構築するためにウィジェットを作ることによってレイアウトを作成する.
例えば,下の図は,3つのアイコンとそれぞれの下にあるラベル,そして対応するウィジェット・ツリーを示している.
この例では,アイコンとラベルを含む3つの列の行がある.
すべてのレイアウトでは,複雑ではなく,これらのウィジェットを組み合わせたものである.
制約条件
Flutterの制約条件を理解することはレイアウトがFlutterでどのように動作するのか理解する重要なことである.
一般的な意味で,レイアウトはウィジェットのサイズとスクリーンにおける位置を参照する.
すべてのウィジェットのサイズと位置は親ウィジェットによって制約される.
そのウィジェットが欲しい任意のサイズになることはできず,スクリーンでどの位置にいるのか決めることができない.
その代わり,大きさと位置はウィジェットとその親ウィジェットとのやり取りから決定される.
最も単純な例では,レイアウトのやり取りは以下のようになる.
- ウィジェットは親ウィジェットから制約を受け取る
- 制約条件は最小幅,最大幅,最小高さ,最大高さの4つの組み合わせのみである
- ウィジェットは制約条件を満たすように幅と高さを決定し,親ウィジェットに幅と高さを返す
- 親ウィジェットは,ウィジェットのサイズとアライメントを確認し,ウィジェットの位置を設定する.整列は,Centerのような様々なウィジェットや,RowとColumnの整列プロパティを使用して,明示的に設定することができる
Flutterでは,レイアウトのやり取りは以下のように要約される.
- サイズ条件を渡すと
- サイズが返ってきて
- 親ウィジェットが位置を設定する
ボックスタイプ
Flutterでは,RenderBox
オブジェクトを通してウィジェットが描かれる.
そのオブジェクトは制約条件のやりとりの仕方を決定する.
一般的に,3種類のボックスがある.
- できる限り大きくする.例えば,
Center
やListView
が使われたとき - 子ウィジェットと同じ大きさにする.例えば,
Transform
やOpacity
が使われたとき - できる限り特定のサイズにする.例えば,
Image
やText
が使われたとき
例えばContainer
のような,いくつかのウィジェットはコンストラクタの引数や型によって異なる.
Container
コンストラクタはできる限り大きくするようにデフォルト設定するが,もし幅を与えられたら,その幅を尊重し,その幅に収める.
Row
やColumn
など他のウィジェットは与えられた条件によって異なる.
フレックスボックスと制約条件について以下の記事をもっと読んだ方がよい.
https://docs.flutter.dev/ui/layout/constraints
単一ウィジェットのレイアウト
Flutterで1つのウィジェットをレイアウトするには,Text
やImage
のような可視ウィジェットを,Center
ウィジェットのような画面上の位置を変更できるウィジェットで包む.
Widget build(BuildContext context) {
return Center(
child: BorderedImage(),
);
}
以下の図について,左側は整列されておらず,右側は中心に置かれている.
すべてのウィジェットは以下のどちらかのメンバ変数を持っている.
-
Center
,Container
,Padding
など一つの子を持つ場合,child
メンバ変数を持つ -
Row
,Column
,ListView
,Stack
など複数の子を持つ場合,children
メンバ変数を持つ
Container
Container
はレイアウト,ペイント,位置決め,サイジングのためにいくつかの便利なウィジェットを組み合わせたものである.
それはウィジェットにパディングとマージンを追加することもできる.
Padding
ウィジェットを使って同じ効果を得ることができる.
以下はCoontainer
の例である.
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.0),
child: BorderedImage(),
);
}
以下の図において,左側はパディングなしで,右側はパディングありである.
Flutterでもっと複雑なレイアウトを作成するために,多くのウィジェットを組み合わせることができる.
例えば,Container
とCenter
を組み合わせることができる.
Widget build(BuildContext context) {
return Center(
Container(
padding: EdgeInsets.all(16.0),
child: BorderedImage(),
),
);
}
垂直や水平のいくつかのウィジェットのレイアウト
最も一般的なレイアウトパターンは垂直方向や水平方向にウィジェットを整列することである.
水平方向に整列するためにRow
ウィジェットを使うことができる.
垂直方向に整列するためにColumn
ウィジェットを使うことができる.
このページの始めの画像を両方とも使っている.
これはもっとも単純なRow
ウィジェットの例である.
Widget build(BuildContext context) {
return Row(
children: [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}

複雑なレイアウトを作成するために,Row
とColumn
の子ウィジェットはさらにRow
とColumn
を入れ子にすることができる.
例えば,Column
を使うことでそれぞれの画像にラベルを付けることができる.
Widget build(BuildContext context) {
return Row(
children: [
Column(
children: [
BorderedImage(),
Text('Dash 1'),
],
),
Column(
children: [
BorderedImage(),
Text('Dash 2'),
],
),
Column(
children: [
BorderedImage(),
Text('Dash 3'),
],
),
],
);
}

RowとColumnの中でウィジェットを整列する
以下の例では,それぞれのウィジェットは200ピクセルの幅があり,表示領域は700ピクセルある.
それぞれのウィジェットは左側から順に並び,余った領域は右側にある.
mainAxisAlignment
とcrossAxisAlignment
のメンバ変数を使って,行または列の子プロパティの整列方法を制御する.
Row
の場合,主軸は水平に,交差軸は垂直に走る.
Column
の場合,主軸は垂直に,交差軸は水平に走る.
主軸のアラインメントをspaceEvenly
に設定することは,各画像の間,前後の自由な水平スペースを均等に分割する.
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}

Column
はRow
と同時に動作する.
以下の例は3つの画像を縦に並べたものであり,画像の高さは100ピクセルである.
レンダーボックスの高さが300ピクセル以上あるため,spaceEvenly
を設定することは,各画像の間,前後の自由な垂直スペースを均等に分割する.

mainAxisAlignment
とcrossAxisAlignment
のEnumはアラインメントのコントロールを行う.
RowやColumnに収まるようにサイジングする
Expanded
ウィジェットを使うことによって,Row
やColumn
に収まるようにウィジェットのサイズを変える.
以前の例にExpanded
ウィジェットを適用すると以下のようになる.
Widget build(BuildContext context) {
return const Row(
children: [
Expanded(
child: BorderedImage(width: 150, height: 150),
),
Expanded(
child: BorderedImage(width: 150, height: 150),
),
Expanded(
child: BorderedImage(width: 150, height: 150),
),
],
);
}

Expanded
ウィジェットは,兄弟ウィジェットに対してどれだけのスペースを取るかを指定することもできる.
例えば,その兄弟ウィジェットに対して2倍の大きさを占めるようなウィジェットを作ることを考える.
このためには,Expanded
ウィジェットのflex
メンバ変数を使う.
これに設定する整数値はウィジェットのflex
係数を決定する.
デフォルト係数は1である.
以下は中央の画像のflex
係数を2,他を1とした例である.
Widget build(BuildContext context) {
return const Row(
children: [
Expanded(
child: BorderedImage(width: 150, height: 150),
),
Expanded(
flex: 2,
child: BorderedImage(width: 150, height: 150),
),
Expanded(
child: BorderedImage(width: 150, height: 150),
),
],
);
}

DevToolsとデバッグレイアウト
場合によって,ボックスの制約条件は境界なしで無限になることがある.
これは,最大幅と最大高さがdouble.infinity
に設定されている.
可能な限り大きくしようとするボックスは,束縛されない制約が与えられたときに有用に機能せず,デバッグモードでは例外を投げる.
レンダーボックスが非束縛制約で終わる最も一般的なケースは、フレックスボックス(Row
またはColumn
)内と、スクロール可能な領域(ListView
や他のScrollView
サブクラスなど)内である.
例えば,ListView
は交差軸で可能な限りスペースに合うように拡張しようとする.
もし,水平方向にスクロール可能なListView
の中に,垂直方向にスクロール可能なListView
があると,中のListView
は可能な限り水平方向に広がろうとする.
外側のListView
は水平方向にスクロール可能なため,無限の幅を持っている.
Flutterアプリをビルドする間に遭遇する一般的なエラーは,レイアウトウィジェットを正しく使わずに発生する非束縛制約に関するエラーである.
Flutterアプリを作り始めたときに直面する覚悟が必要なエラーが1つだけあるとすれば,それはこれだろう.
https://www.youtube.com/watch/jckqXR5CrPI
スクロール可能なウィジェット
Flutterはスクロール可能な多くの内蔵ウィジェットを持っており,特定のスクロールする動作を作成するためのカスタマイズできる多くのウィジェットがある.
このページでは,スクロール可能なリストを作成するためのウィジェットと同様の,スクロール可能な一般的なウィジェットを作成する.
ListView
ListView
はコンテンツがレンダーボックスよりはみ出た場合,自動的にスクロール可能にするColumn
のようなウィジェットである.
ListView
を使う最も基本的な方法は,Column
やRow
と同様に使うことである.
Column
やRow
と違って,ListView
はその子ウィジェットが横軸の利用可能なスペースをすべて占有する必要がある.
例を以下に示す.
Widget build(BuildContext context) {
return ListView(
children: const [
BorderedImage(),
BorderedImage(),
BorderedImage(),
],
);
}

とても長いコンテンツを表示したいときにListView
を使う.
この場合,ListView.builder
コンストラクタを使うのが一般的である.
builder
コンストラクタは画面上で見ることのできる子ウィジェットのbuild
メソッドを一度のみ呼び出す.
以下の例では,ListView
はto-doアイテムを表示している.
このtodoアイテムはリポジトリからフェッチするため,todoの数は分からない.
final List<ToDo> items = Repository.fetchTodos();
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, idx) {
var item = items[idx];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.description),
Text(item.isComplete),
],
),
);
},
);
}

適応的なレイアウト
Flutterはモバイル・タブレット・デスクトップ・ウェブアプリに使われるため,スクリーンサイズや入力デバイスのように環境が異なる中でアプリケーションを適合させる必要がある.
これは適合可能でレスポンシブなアプリを作るのに必要である.
適合可能なウィジェットを作るためによく使われるウィジェットはLayoutBuilder
ウィジェットである.
LayoutBuilder
ウィジェットはFlutterでbuilder
パターンを使うウィジェットである.
Builderパターン
Flutterでは,builderと名前のつくコンストラクタを見つけるだろう.
以下のリストがすべてではない.
- ListView.builder
- GridView.builder
- Builder
- LayoutBuilder
- FutureBuilder
これらの異なるBuilderは異なる問題を解決するために使われる.
例えば,ListView.builder
コンストラクタは主にリスト内のアイテムを遅延レンダリングするために使われ,Builder
ウィジェットは深いウィジェットコードでBuildContext
にアクセスするのに使われる.
異なるユースケースであるにもかかわらず,これらのbuilderはどのような動作をするかで統一されている.
Builder
ウィジェットとbuilder
コンストラクタすべてはbuilder
と呼ばれる引数を持つ.
例えば,ListView.builder
の場合,itemBuilder
になる場合もある.
また,builder
引数はコールバックも受付可能である.
このコールバックはbuilder関数と呼ばれる.
builder関数は親ウィジェットにデータを渡すウィジェットであり,子ウィジェットを作成し返すための引数を親ウィジェットが使う.
builder関数はいつも,少なくとも1つの引数,通常は2つの引数を渡す.
例えば,LayoutBuilder
ウィジェットは画面サイズを基にレスポンシブなレイアウトを作成するために使われる.
builderコールバックの本体は,親から受け取ったBoxConstraints
と,ウィジェットのBuildContext
が渡される.
この制約条件のため,利用可能なスペースを基に異なるウィジェットを返すことができる.
https://www.youtube.com/watch/IYDVcriKjsw
以下の例では,画面の幅が600ピクセルかどうかでウィジェットを変更するLayoutWidget
となっている.
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth <= 600) {
return _MobileLayout();
} else {
return _DesktopLayout();
}
},
);
}

一方で,ListView.builder
コンストラクタのitemBuilder
コールバックはbuild内容とintが渡される.
このコールバックはリスト内のすべての要素に対して一度だけ呼ばれる.
そして,int引数はリストのどの要素なのか指定する.
flutterがUIを作成するときに初めてitemBuilder
コールバックが呼ばれ,intは0から順に渡される.
インデックスを基にした特殊な設定を行うことができる.
上記のListView.builder
コンストラクタを使用した例を示す.
final List<ToDo> items = Repository.fetchTodos();
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, idx) {
var item = items[idx];
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.description),
Text(item.isComplete),
],
),
);
},
);
}
このコード例は,ビルダーに渡されたインデックスを使用して,アイテムのリストから正しいToDoを取得し,ビルダーから返されたウィジェットにそのToDoのデータを表示する.
これを例示するために,以下のコード例は,背景の色を変えるようにしている.
final List<ToDo> items = Repository.fetchTodos();
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, idx) {
var item = items[idx];
return Container(
color: idx % 2 == 0 ? Colors.lightBlue : Colors.transparent,
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item.description),
Text(item.isComplete),
],
),
);
},
);
}

追加の項目
ここを見て.
https://docs.flutter.dev/get-started/fundamentals/layout#additional-resources
APIリファレンス
ここを見て.
https://docs.flutter.dev/get-started/fundamentals/layout#api-reference