公開日 2026-06-23

Flutterのレイアウト入門(Column / Row / Stack の使い分け)

Flutter の Column / Row / Stack を最小例で整理し、RenderFlex overflow の原因と Expanded / Flexible の使い分けまで判断できるようにする。

目次

  1. 1. ゴールと非対象
  2. 対象読者
  3. この記事で到達する状態
  4. 非対象
  5. 2. まずは 3 つの役割を分ける
  6. 3. Column は縦に並べる
  7. コードのポイント
  8. 4. Row でよく出る overflow の正体
  9. コードのポイント
  10. コードのポイント
  11. 5. Expanded と Flexible の違い
  12. コードのポイント
  13. 6. Stack は重ねる
  14. コードのポイント
  15. 7. 迷ったときの使い分け
  16. 8. まとめ

FlutterでBuildContextとKeyを理解する の次に、UI を組み始めるとすぐ出てくるのが Column Row Stack です。ここで止まりやすいのは、Widget 名より「どの方向へ並べるのか」「なぜ overflow が出るのか」が曖昧なまま進めてしまうからです。この記事では、レイアウト入門として最初に押さえたい 3 つと、Expanded / Flexible の違いまでを最小例で整理します。

1. ゴールと非対象

対象読者

  • Flutter の環境構築と Dart 入門までは終わった人
  • RowColumn を見ても、どちらを選ぶかまだ曖昧な人
  • RenderFlex overflowed を見て、何から直せばよいか分からない人

この記事で到達する状態

  • ColumnRow を縦横の違いだけでなく、主軸と交差軸で読める
  • overflow が出る理由を、親の幅と子の幅の関係で説明できる
  • ExpandedFlexible のどちらを選ぶか判断できる
  • Stack を「重ねる場面」に限定して使える

非対象

  • ListView / GridView / CustomScrollView
  • Wrap / LayoutBuilder / MediaQuery の詳細
  • レスポンシブ設計の深掘り
  • アニメーションやテーマ設計

Flutter のレイアウトは、見た目より先に「親からどんな制約が渡されるか」を見るほうが整理しやすくなります。最初の入口では、全部を覚える必要はありません。まずは 縦に並べる 横に並べる 重ねる の 3 つを分けて読めれば十分です。

2. まずは 3 つの役割を分ける

最初に、Column Row Stack の役割を分けます。

Widget役割まず見るポイントよくある場面
Column子要素を縦に並べる主軸は縦、交差軸は横フォーム、プロフィール、詳細画面
Row子要素を横に並べる主軸は横、交差軸は縦ヘッダー、アイコン + テキスト、ボタン横並び
Stack子要素を前後に重ねる重ね順と位置指定通知バッジ、画像の上のラベル

ExpandedFlexible は、これらとは役割が違います。ColumnRow の中で「残りの領域をどう配るか」を決める補助です。先にこの役割分担を持っておくと、Widget 名を見た時点で判断しやすくなります。

以降のコード例はすべて lib/main.dart にそのまま貼って flutter run で確認できます。外部パッケージ不要のため、DartPad に貼って実行することもできます。

3. Column は縦に並べる

Column は子要素を上から下へ並べます。ここで一緒に覚えたいのが、主軸と交差軸です。

  • Column の主軸: 縦方向
  • Column の交差軸: 横方向

mainAxisAlignment は主軸方向の並び方、crossAxisAlignment は交差軸方向の揃え方を決めます。

lib/main.dart の最小例です。

このファイルは、Column を置いたときに主軸と交差軸がどちらに効くかを一度に確認するための最小例です。横幅を持った Container の中へ置いているので、stretch と縦方向の間隔も同時に観察できます。

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(
      home: Scaffold(
        appBar: AppBar(title: const Text('Column sample')),
        body: Center(
          child: Container(
            width: 280,
            padding: const EdgeInsets.all(16),
            color: Colors.blueGrey.shade50,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: const [
                Text(
                  '在庫確認',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 8),
                Text('倉庫A / 棚12-3'),
                SizedBox(height: 12),
                Text('今日の更新件数: 18'),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

コードのポイント

Column では縦方向が主軸になる

            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: const [

Column を見たら、まず主軸が縦だと読むのが出発点です。mainAxisSize が縦方向の取り方、crossAxisAlignment が横方向の揃え方を担当しているため、どの軸へ効く設定かを切り分けて追えます。

SizedBox(height: ...) で縦方向の間隔を明示する

              children: const [
                Text(
                  '在庫確認',
                  style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                SizedBox(height: 8),
                Text('倉庫A / 棚12-3'),
                SizedBox(height: 12),
                Text('今日の更新件数: 18'),
              ],

縦方向の gap を SizedBox(height: ...) で入れているので、本文と補足情報の距離がコード上でもすぐ分かります。Column の子要素は上から下へ積まれるため、この height がそのまま縦の間隔として効きます。

Column サンプルの実行結果。在庫確認カードが縦方向に積まれている

4. Row でよく出る overflow の正体

Row は子要素を左から右へ並べます。主軸は横方向で、交差軸は縦方向になります。

ここで止まりやすいのは、長いテキストや複数のボタンを横に並べた場面です。RenderFlex overflowed by ... pixels on the right が出るのは、親が渡した横幅より、子要素が必要とする横幅の合計が大きいためです。

まずは overflow が起きる例を見ます。

このファイルは、Row に可変長テキストをそのまま置くと何が起きるかを再現するための最小例です。Icon、長い Text、ボタンを横一列に置き、親の幅を超えたときの典型的な詰まり方をわざと作っています。

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(
      home: Scaffold(
        appBar: AppBar(title: const Text('Row overflow')),
        body: Center(
          child: Row(
            children: [
              const Icon(Icons.inventory_2),
              const SizedBox(width: 8),
              const Text('棚卸し結果をサーバーへ送信して処理結果を表示します'),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: () {},
                child: const Text('送信'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

コードのポイント

Row の子が全員自然幅を要求すると横幅が足りなくなる

          child: Row(
            children: [
              const Icon(Icons.inventory_2),
              const SizedBox(width: 8),
              const Text('棚卸し結果をサーバーへ送信して処理結果を表示します'),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: () {},
                child: const Text('送信'),
              ),
            ],

この Text は自分が必要とする自然幅をそのまま取りにいきます。Row の中でアイコン、長いテキスト、ボタンが全員好きな幅を要求すると、親の横幅に収まらず overflow になります。

Row overflow の実行結果。右端に黄黒の警告ストライプが出て、RIGHT OVERFLOWED BY 9.9 PIXELS と表示されている
flowchart LR
  Parent[親ウィジェットの横幅 320] --> Row[Row]
  Row --> Icon[Icon 24]
  Row --> Gap1[余白 8]
  Row --> Text[長い Text の自然幅 260+]
  Row --> Gap2[余白 8]
  Row --> Button[Button 80]
  Text --> Sum[合計幅が 320 を超える]
  Button --> Sum
  Sum --> Overflow[RenderFlex overflow]

直し方の基本は、長さが変わる子へ「残りの幅の中で使ってよい範囲」を渡すことです。そのために ExpandedFlexible を使います。

このファイルは、同じ RowExpanded 付きに変えて、可変長の Text へ残り幅を割り当てる例です。overflow が起きる原因を保ったまま、制約の渡し方だけを変えています。

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(
      home: Scaffold(
        appBar: AppBar(title: const Text('Row + Expanded')),
        body: Center(
          child: Row(
            children: [
              const Icon(Icons.inventory_2),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  '棚卸し結果をサーバーへ送信して処理結果を表示します',
                  maxLines: 2,
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              const SizedBox(width: 8),
              ElevatedButton(
                onPressed: () {},
                child: const Text('送信'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

コードのポイント

① 可変長の TextExpanded を付けて残り幅へ収める

Expanded(
  child: Text(
    '棚卸し結果をサーバーへ送信して処理結果を表示します',
    maxLines: 2,
    overflow: TextOverflow.ellipsis,
  ),
),

Expanded を付けると、この Text は残り領域の中で描かれる前提に変わります。Row の overflow を見たときは、可変長の子が自然幅のまま置かれていないかを最初に疑うと原因を絞りやすくなります。

② 収まらない文は maxLinesellipsis で切り詰める

  child: Text(
    '棚卸し結果をサーバーへ送信して処理結果を表示します',
    maxLines: 2,
    overflow: TextOverflow.ellipsis,
  ),

幅を制約しただけでは、長い文をどう見せるかまでは決まりません。maxLinesoverflow を合わせると、残り幅へ収めながら「切れてよい表示」であることもコードに残せます。

Expanded を入れた後の実行結果。テキストが省略されてボタンが右端に収まっている

5. ExpandedFlexible の違い

どちらも Row / Column の中で残り領域を扱う Widget です。ただし、埋め方が違います。

  • Expanded: 残り領域を埋める前提で子に幅や高さを渡す
  • Flexible: 子が必要とするサイズも尊重しつつ、収まる範囲で縮める

比較しやすい最小例です。

このファイルは、ExpandedFlexible が同じ Row の中でどう振る舞い分かれるかを見比べるための最小例です。固定幅の箱を左に置いて残り領域を作り、2つの Widget がその空き方をどう使うかに注目します。

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(
      home: Scaffold(
        appBar: AppBar(title: const Text('Expanded vs Flexible')),
        body: Center(
          child: Row(
            children: [
              Container(
                width: 56,
                height: 56,
                color: Colors.blue.shade100,
                alignment: Alignment.center,
                child: const Text('固定'),
              ),
              const SizedBox(width: 8),
              Expanded(
                child: Container(
                  height: 56,
                  color: Colors.green.shade100,
                  alignment: Alignment.center,
                  child: const Text('Expanded'),
                ),
              ),
              const SizedBox(width: 8),
              Flexible(
                fit: FlexFit.loose,
                child: Container(
                  height: 56,
                  padding: const EdgeInsets.symmetric(horizontal: 12),
                  color: Colors.orange.shade100,
                  alignment: Alignment.center,
                  child: const Text('Flexible'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

コードのポイント

Expanded は残り領域を埋める前提で子に幅を渡す

Expanded(
  child: Container(
    height: 56,
    color: Colors.green.shade100,
    alignment: Alignment.center,
    child: const Text('Expanded'),
  ),
),

Expanded 側は、空いている幅を積極的に使う前提でレイアウトされます。一覧タイトルや本文欄のように、使える横幅をしっかり消費したい子に向いています。

Flexible は収まる範囲で必要なぶんだけ広がる

Flexible(
  fit: FlexFit.loose,
  child: Container(
    height: 56,
    padding: const EdgeInsets.symmetric(horizontal: 12),
    color: Colors.orange.shade100,
    alignment: Alignment.center,
    child: const Text('Flexible'),
  ),
),

Flexible は弱い Expanded ではなく、収まる範囲で十分な場面に使う選択肢です。ラベルや補助ボタンのように、必要以上に横へ引き伸ばしたくない子にはこちらのほうが意図に合います。

Expanded vs Flexible の実行結果。Expanded が残り領域を広く使い、Flexible はテキスト幅に近い範囲に収まっている

6. Stack は重ねる

Stack は子要素を前後に重ねるための Widget です。縦横どちらへ並べるかではなく、「同じ領域に重ねたいか」を見ると選びやすくなります。

このファイルは、商品アイコンの土台と件数バッジを重ねて、Stack が向く場面を最小構成で見せる例です。単に並べるのではなく、同じ領域の上へ別要素を載せるのが主題です。

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(
      home: Scaffold(
        appBar: AppBar(title: const Text('Stack sample')),
        body: Center(
          child: Stack(
            clipBehavior: Clip.none,
            children: [
              Container(
                width: 96,
                height: 96,
                decoration: BoxDecoration(
                  color: Colors.blueGrey.shade100,
                  borderRadius: BorderRadius.circular(12),
                ),
                alignment: Alignment.center,
                child: const Icon(Icons.inventory_2, size: 40),
              ),
              Positioned(
                top: -6,
                right: -6,
                child: Container(
                  padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                  decoration: BoxDecoration(
                    color: Colors.red,
                    borderRadius: BorderRadius.circular(999),
                  ),
                  child: const Text(
                    '3',
                    style: TextStyle(color: Colors.white),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

コードのポイント

Stack の子は同じ領域へ前後関係つきで重なる

child: Stack(
  clipBehavior: Clip.none,
  children: [
    Container(
      width: 96,
      height: 96,

Stack を選ぶ理由は、縦横の並びではなく重なりです。土台のコンテナとバッジを同じ座標系で扱えるため、「上に載せる」表現が素直に書けます。

Positioned で重ねる位置を明示する

Positioned(
  top: -6,
  right: -6,
  child: Container(
    padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),

バッジの位置は Positioned で明示しておくと、右上へ少しはみ出させたい意図がコード上で読み取れます。単に 2 列で並べたいだけなら RowColumn のほうが追いやすいため、重ねたいときにだけ Stack を使うのが安全です。

Stack sample の実行結果。商品アイコンの右上に赤いバッジが重なっている

7. 迷ったときの使い分け

手元の画面で迷ったときは、まず「何をしたいか」を次の表へ戻すと整理しやすくなります。

やりたいこと最初に選ぶ Widget一緒に見るもの
入力欄や説明文を上から下へ積むColumnmainAxisAlignment / crossAxisAlignment
アイコン、タイトル、操作ボタンを横に並べるRow可変長の子に Expanded / Flexible が要るか
画像やアイコンの上にバッジやラベルを載せるStackPositioned でどこへ置くか
長いテキストで右にはみ出すRow + ExpandedmaxLines / overflow
子を無理に広げたくないFlexiblefit: FlexFit.loose

チェック順も短く決めておくと楽です。

  1. 縦に並べるのか、横に並べるのか、重ねるのかを決める
  2. 可変長の子があるなら、残り領域の配り方を確認する
  3. overflow が出たら、長い子をそのまま置いていないか見る

8. まとめ

Column は縦に並べる、Row は横に並べる、Stack は重ねる。この役割分担を先に持っておくと、Flutter のレイアウトは読み順が安定します。

Row で overflow が出たときは、Widget 名より先に「親の幅に対して、どの子が自然幅を取りすぎているか」を見ます。そこで残り領域を配るのが ExpandedFlexible です。

次に UI 基礎を進めるなら、Container / SizedBox / Padding の使い分けか、ListView / GridView の一覧画面に進むと流れがつながります。レイアウトで詰まったら、まずは方向、次に残り領域、最後に重ね表示の必要有無を確認してください。

シリーズ 8/14

このシリーズ

Flutter導入と基礎

  1. 1. Windows 11で始めるFlutter開発環境:Android Emulatorで動かすまで
  2. 2. Flutter + FVM で開発環境のバージョンを固定する
  3. 3. Flutterで画像・SVG・アイコンを管理する(flutter_gen最小構成)
  4. 4. Flutterで最初に詰まりやすいDartの書き方:final・const・null safety・async/await を最初に整理する
  5. 5. DartのStream入門(非同期データの流れをつかむ)
  6. 6. FlutterのWidgetライフサイクル入門(initState / dispose で詰まらないために)
  7. 7. FlutterでBuildContextとKeyを理解する
  8. 8. Flutterのレイアウト入門(Column / Row / Stack の使い分け) 現在の記事
  9. 9. Flutterのテーマ設計入門(ThemeData + Theme Extension)
  10. 10. FlutterでMediaQueryとLayoutBuilderを使って画面サイズに対応する(スマホ・タブレット両対応)
  11. 11. FlutterのContainerとSizedBoxを使いこなす(余白・サイズ・装飾の基本)
  12. 12. FlutterのListViewとGridViewで一覧画面を作る(基本パターン)
  13. 13. Flutterのダイアログ・スナックバー・ボトムシートを使う(確認・通知UIの基本)
  14. 14. FlutterのTabBarとBottomNavigationBarで複数画面を切り替える