🐥

Flutterのレイアウトの仕組みをやさしく解説!「Constraints go down, sizes go up, parent set

に公開

Flutterのレイアウトを理解するうえで非常に重要な原則に、
「Constraints go down, sizes go up, parent sets the position」
というものがあります。これは Flutter のレイアウトシステムを端的に表したフレーズですが、初めて Flutter を触るエンジニアの方には一見わかりづらいかもしれません。

本記事では、このフレーズがどういう意味で、実際にどのような挙動を示すのか、具体的な例を挙げながら解説します。


1. ざっくり解説

  1. Constraints go down (制約は下に伝わる)
    親ウィジェット(Parent) は子ウィジェット(Child) に対して「これくらいの幅と高さの範囲で収まってください」という「制約(Constraints)」を渡します。たとえば「最小幅 0、最大幅 300」のような情報が下に伝わります。

  2. Sizes go up (サイズは上に返される)
    子ウィジェットは、親から渡された制約の中で「自分はこの大きさ(Width x Height)になります」とサイズを決定し、それを親に返します。

  3. Parent sets the position (最終的な配置は親が決める)
    子ウィジェットの「最終的な表示位置(座標)」は、親ウィジェットが決定します。
    たとえば、Row や Column のようなレイアウトウィジェットは、子のサイズを考慮しながら、それぞれの並び順や配置を決めます。


2. シンプルな実例

2.1. Container + Text

Widget build(BuildContext context) {
  return Container(
    width: 200,
    height: 100,
    color: Colors.blue,
    child: Text('Hello Flutter!'),
  );
}
  • Constraints go down
    • 画面全体 → Container → Text という流れで制約が降りてきます。
    • ここでは Container(width: 200, height: 100) を指定しているため、Container が “幅 200、高さ 100 以内” という自分の制約を Text にさらに伝えます。
  • Sizes go up
    • Text ウィジェットは文字列を描画するときの最小サイズが必要ですが、親 Container の制約 (最大 200×100 の範囲) に収まるように自分のサイズを決定し、それを親に返します。
  • Parent sets the position
    • Container は自分の子である Text を左上から配置する (デフォルトのアルゴリズム)。
    • 実際には Container が余白を加えるなど別の設定をすることも可能です。

この例のように、単純な Container と Text の組み合わせでも、「最小~最大サイズの制約」と「実際に取ったサイズ」のやりとり、そして「どこに置くか」を決定する流れが発生しています。


3. Row / Column の場合

3.1. Row ウィジェットの例

Widget build(BuildContext context) {
  return Row(
    children: [
      Container(width: 100, height: 50, color: Colors.red),
      Container(width: 100, height: 50, color: Colors.green),
      Container(width: 100, height: 50, color: Colors.blue),
    ],
  );
}
  • Constraints go down
    • Row は親(画面全体や上位レイアウト)の制約を受け取ります。(例:画面幅が 400 ピクセルだとして、最大幅 400, 高さは無制限など)
    • 次に Row は “horizontal layout” の仕組みに則って、子コンテナたちに幅と高さの制約を与えます。たとえば、Row の中の子には「幅の上限はある程度あるが高さはいくらでもOK(実際には親の制約や Row の高さなどによる)」という制約を渡します。
  • Sizes go up
    • 各子コンテナは受け取った制約に従って、「自分は width: 100, height: 50」というサイズになる、と親 (Row) に返します。
  • Parent sets the position
    • Row は、それぞれの子コンテナを横方向に順番に配置します。最終的に、最初の子を x=0 の位置、次の子を x=100 の位置、… という形で配置していきます。

もし画面幅が狭い場合、Row の制約が「最大 250 ピクセル」程度だとすると、3 つのコンテナの合計幅は 300 ピクセルになるため入りきりません。その場合は Row がオーバーフローエラーを出すか、スクロールできるようにするかなど、結果は状況に応じて変化します。これも、親 → 子に制約が渡され、子がサイズを返し、それを親が配置しようとした結果として起こります。


4. サイズと配置をもっと体感する例

4.1. Expanded や Flexible を使った例

例えば、以下のように Row で Expanded を使うと、子ウィジェットが “利用可能な領域いっぱいに広がる” 挙動を取ります。

Widget build(BuildContext context) {
  return Row(
    children: [
      Expanded(
        child: Container(color: Colors.red),
      ),
      Container(width: 100, color: Colors.green),
    ],
  );
}
  1. 親からの制約 → Row が受け取る
    • 画面幅が 400 ピクセルなら、Row は “最大幅 400” という制約を持つ。
  2. Row → 子どもに制約を渡す
    • Expanded の子 Container には “残りの幅を全部使って OK” という制約が渡される。
    • もう一つの子 Container(width: 100) には “幅 100、最大高さは親に依存” という制約が渡される。
  3. Expanded の子はその制約の中で「自分の幅は残り全部」とサイズを決定
    • もし画面幅が 400 ピクセルだとすれば、1 番目の子が幅 300、2 番目の子が width=100 などになる。
  4. Row が最終的に子どもを配置
    • 1 番目の子を左端に、2 番目の子を 1 番目の子の右側 (x = 300) に配置。

大事なポイント

  • Expanded や Flexible は、親ウィジェットが子ウィジェットに与える制約 を動的に調整する仕組みを提供してくれます。
  • 子ウィジェットはその制約を受け、ちょうど必要な分のサイズを報告し、最終的に親 (Row) が位置を決めます。

5. 親がサイズを指定しない場合どうなる?

Flutter では、例えば Column の子に Container を置いても、親が明確な高さを指定しない場合、
「Constraints go down はどうなるの?」という疑問が生まれます。

5.1. Column + Container の例

Widget build(BuildContext context) {
  return Column(
    children: [
      Container(
        color: Colors.red,
        child: Text("Hello"),
      ),
      Container(
        color: Colors.blue,
        child: Text("World"),
      ),
    ],
  );
}
  • Column 自体が画面全体に対して「横幅は画面幅いっぱいにできるが、高さは必要に応じてどこまででも伸びられる」という制約を受け取ります(スクロール可否や画面設計による)。
  • Column は「縦方向に並べる」という性質を持つため、子 Container に縦方向の具体的な最大値を与えず、横方向だけ「最大幅は画面幅」という制約を渡します。
  • 各 Container は、子の Text が必要とする分だけ高さを取ります (Constraints go down, sizes go up)。
  • それぞれの Container が算出した高さを Column に返し、Column は順番に積み重ねて配置 (Parent sets position) します。

結果、テキストの行数が増えればコンテナの高さも伸びていき、画面がスクロールの必要な高さまで大きくなる場合もあります。


6. 「Constraints go down, sizes go up, parent sets position」をまとめる

  1. Constraints go down
    • 親が子に、「これくらいの幅・高さで収まってほしい」という制約を伝える。
  2. Sizes go up
    • 子は親からもらった制約をもとに、「自分はこれくらいのサイズになります」と計算し、親に報告する。
  3. Parent sets the position
    • 親は、子の最終的な配置 (座標) を決定し、画面上に描画する。

このように、Flutter のレイアウトは 単純な 3 ステップのやりとり で決まります。ただし、Expanded や Flex、IntrinsicWidth などのレイアウトウィジェットを組み合わせると、より複雑な制約のやりとりが行われるようになります。


7. 補足:Widget ツリーと RenderObject ツリー

Flutter は、Widget、Element、RenderObject という階層的な仕組みで構成されています。この「Constraints go down, sizes go up, parent sets the position」は、実際には RenderObject レベルのレイアウトプロセスで行われています。

  • Widget: 設定や構造を記述する宣言的な存在
  • Element: Widget のインスタンスと RenderObject をつなぐ
  • RenderObject: レイアウトや描画を司る低レベルオブジェクト

ただ、通常のアプリ開発では Widget で構造を組むだけで十分であり、その裏で RenderObject のレイアウトシステムがこのプロセスを遂行していると捉えていただければ OK です。


8. まとめ

  • Constraints go down: 親が子にレイアウトの「幅・高さ」の範囲を渡す
  • Sizes go up: 子はその制約に従って必要サイズを計算し、親に返す
  • Parent sets the position: 親が子の最終配置位置を決めて描画

これが Flutter レイアウトの基本原則です。
複雑な UI を作るときも、この仕組みをベースに理解を進めると「なぜ予想と違う見た目になったのか」「どうすれば希望のレイアウトにできるのか」がわかりやすくなります。

初めて Flutter を使うエンジニアの方は、まずはこのルールを頭に入れながら、Container、Row、Column などの基本ウィジェットでどのようにサイズや配置が行われるのか試行錯誤してみてください。
少しずつ Flutter のレイアウトに慣れてきて、思い通りの UI を作成できるようになるはずです!

Discussion