FlutterでBuildContextとKeyを理解する の次に、UI を組み始めるとすぐ出てくるのが Column Row Stack です。ここで止まりやすいのは、Widget 名より「どの方向へ並べるのか」「なぜ overflow が出るのか」が曖昧なまま進めてしまうからです。この記事では、レイアウト入門として最初に押さえたい 3 つと、Expanded / Flexible の違いまでを最小例で整理します。
1. ゴールと非対象
対象読者
- Flutter の環境構築と Dart 入門までは終わった人
RowやColumnを見ても、どちらを選ぶかまだ曖昧な人RenderFlex overflowedを見て、何から直せばよいか分からない人
この記事で到達する状態
ColumnとRowを縦横の違いだけでなく、主軸と交差軸で読める- overflow が出る理由を、親の幅と子の幅の関係で説明できる
ExpandedとFlexibleのどちらを選ぶか判断できるStackを「重ねる場面」に限定して使える
非対象
ListView/GridView/CustomScrollViewWrap/LayoutBuilder/MediaQueryの詳細- レスポンシブ設計の深掘り
- アニメーションやテーマ設計
Flutter のレイアウトは、見た目より先に「親からどんな制約が渡されるか」を見るほうが整理しやすくなります。最初の入口では、全部を覚える必要はありません。まずは 縦に並べる 横に並べる 重ねる の 3 つを分けて読めれば十分です。
2. まずは 3 つの役割を分ける
最初に、Column Row Stack の役割を分けます。
| Widget | 役割 | まず見るポイント | よくある場面 |
|---|---|---|---|
Column | 子要素を縦に並べる | 主軸は縦、交差軸は横 | フォーム、プロフィール、詳細画面 |
Row | 子要素を横に並べる | 主軸は横、交差軸は縦 | ヘッダー、アイコン + テキスト、ボタン横並び |
Stack | 子要素を前後に重ねる | 重ね順と位置指定 | 通知バッジ、画像の上のラベル |
Expanded と Flexible は、これらとは役割が違います。Column や Row の中で「残りの領域をどう配るか」を決める補助です。先にこの役割分担を持っておくと、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 がそのまま縦の間隔として効きます。
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 になります。
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]
直し方の基本は、長さが変わる子へ「残りの幅の中で使ってよい範囲」を渡すことです。そのために Expanded や Flexible を使います。
このファイルは、同じ Row を Expanded 付きに変えて、可変長の 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('送信'),
),
],
),
),
),
);
}
}
コードのポイント
① 可変長の Text へ Expanded を付けて残り幅へ収める
Expanded(
child: Text(
'棚卸し結果をサーバーへ送信して処理結果を表示します',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Expanded を付けると、この Text は残り領域の中で描かれる前提に変わります。Row の overflow を見たときは、可変長の子が自然幅のまま置かれていないかを最初に疑うと原因を絞りやすくなります。
② 収まらない文は maxLines と ellipsis で切り詰める
child: Text(
'棚卸し結果をサーバーへ送信して処理結果を表示します',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
幅を制約しただけでは、長い文をどう見せるかまでは決まりません。maxLines と overflow を合わせると、残り幅へ収めながら「切れてよい表示」であることもコードに残せます。
5. Expanded と Flexible の違い
どちらも Row / Column の中で残り領域を扱う Widget です。ただし、埋め方が違います。
Expanded: 残り領域を埋める前提で子に幅や高さを渡すFlexible: 子が必要とするサイズも尊重しつつ、収まる範囲で縮める
比較しやすい最小例です。
このファイルは、Expanded と Flexible が同じ 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 ではなく、収まる範囲で十分な場面に使う選択肢です。ラベルや補助ボタンのように、必要以上に横へ引き伸ばしたくない子にはこちらのほうが意図に合います。
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 列で並べたいだけなら Row や Column のほうが追いやすいため、重ねたいときにだけ Stack を使うのが安全です。
7. 迷ったときの使い分け
手元の画面で迷ったときは、まず「何をしたいか」を次の表へ戻すと整理しやすくなります。
| やりたいこと | 最初に選ぶ Widget | 一緒に見るもの |
|---|---|---|
| 入力欄や説明文を上から下へ積む | Column | mainAxisAlignment / crossAxisAlignment |
| アイコン、タイトル、操作ボタンを横に並べる | Row | 可変長の子に Expanded / Flexible が要るか |
| 画像やアイコンの上にバッジやラベルを載せる | Stack | Positioned でどこへ置くか |
| 長いテキストで右にはみ出す | Row + Expanded | maxLines / overflow |
| 子を無理に広げたくない | Flexible | fit: FlexFit.loose |
チェック順も短く決めておくと楽です。
- 縦に並べるのか、横に並べるのか、重ねるのかを決める
- 可変長の子があるなら、残り領域の配り方を確認する
- overflow が出たら、長い子をそのまま置いていないか見る
8. まとめ
Column は縦に並べる、Row は横に並べる、Stack は重ねる。この役割分担を先に持っておくと、Flutter のレイアウトは読み順が安定します。
Row で overflow が出たときは、Widget 名より先に「親の幅に対して、どの子が自然幅を取りすぎているか」を見ます。そこで残り領域を配るのが Expanded と Flexible です。
次に UI 基礎を進めるなら、Container / SizedBox / Padding の使い分けか、ListView / GridView の一覧画面に進むと流れがつながります。レイアウトで詰まったら、まずは方向、次に残り領域、最後に重ね表示の必要有無を確認してください。