Flutterのテーマ設計入門(ThemeData + Theme Extension) の次に、UI を組み始めると迷いやすいのが Container と SizedBox の使い分けです。止まりやすいのは API の数ではなく、「間隔を空けたいだけなのに Container を置く」「内側の余白と外側の余白が混ざる」といった役割の曖昧さにあります。この記事では、SizedBox Padding Container を余白・サイズ・装飾の観点で整理し、最初の画面を読みやすく組むための基準をまとめます。
1. ゴールと非対象
対象読者
- Flutter の環境構築と Dart 入門、
Column/Row/Stackの基本までは終わった人 ContainerとSizedBoxをどちらも何となく置いている人- 余白、サイズ、背景色をどこへ書くかまだ曖昧な人
この記事で到達する状態
SizedBoxを間隔と固定サイズのために使えるPaddingとmarginを内側と外側の余白で説明できるContainerを装飾や配置が必要な場面に絞って使える- 「とりあえず
Container」を減らす判断基準を持てる
非対象
ConstrainedBox/Expanded/Flexibleの詳細- レスポンシブ設計の深掘り
- テーマ設計やアニメーション
BoxConstraintsの細かな仕様
この記事で押さえたいのは、レイアウトシステム全体ではありません。余白、サイズ、装飾をどの Widget へ置くかを分けることです。ここが整理できると、一覧やフォームへ進んだあとも、どこで余白や装飾を直すか追いやすくなります。
2. 先に 3 つの役割を分ける
最初に、SizedBox Padding Container の役割を分けます。
| Widget | 主な役割 | まず見るポイント | よくある場面 |
|---|---|---|---|
SizedBox | 固定の余白、固定サイズ | width / height | 部品同士の間隔、画像やボタンの大きさ |
Padding | 内側の余白 | EdgeInsets | カードの内側、テキスト周囲の余白 |
Container | 箱のサイズ、背景、枠線、角丸、配置 | padding / margin / alignment / decoration | カード、ラベル、通知枠 |
Container は何でもできますが、毎回そこへ寄せる必要はありません。役割が小さい場面では、専用 Widget のほうが意図を読み取りやすくなります。
以降のコード例はすべて lib/main.dart にそのまま貼って flutter run で確認できます。外部パッケージ不要のため、DartPad でも試せます。
3. SizedBox は間隔と固定サイズに使う
SizedBox は「空白を入れる Widget」と覚えがちですが、それだけだと用途が狭く見えます。実際には、幅や高さを固定したいときに使う小さな箱です。
次の lib/main.dart では、縦の間隔、横の間隔、固定サイズの 3 つをまとめて確認できます。
このファイルは、SizedBox が gap と固定サイズの両方に使えることを 1 画面で確認するための最小例です。カード、行内の余白、ボタンの高さを同じサンプルへまとめ、役割の違いが見えるようにしています。
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('SizedBox sample')),
body: Center(
child: Container(
width: 280,
padding: const EdgeInsets.all(16),
color: Colors.blueGrey.shade50,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'入荷予定',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
children: [
Container(
width: 56,
height: 56,
color: Colors.blue.shade100,
alignment: Alignment.center,
child: const Text('A'),
),
const SizedBox(width: 12),
const Expanded(
child: Text('棚A-12 に 24 件の入荷予定があります'),
),
],
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton(
onPressed: () {},
child: const Text('詳細を見る'),
),
),
],
),
),
),
),
);
}
}
コードのポイント
① SizedBox(height: ...) と SizedBox(width: ...) で gap を明示する
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'入荷予定',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
children: [
Container(
width: 56,
height: 56,
color: Colors.blue.shade100,
alignment: Alignment.center,
child: const Text('A'),
),
const SizedBox(width: 12),
縦方向と横方向の間隔を SizedBox で分けて書くと、「ここは余白だけが役割だ」とそのまま読めます。背景や枠線を持たないため、Container を gap のためだけに置いたときより責務が小さく保てます。
② ボタンのサイズも SizedBox で固定できる
SizedBox(
width: double.infinity,
height: 44,
child: ElevatedButton(
onPressed: () {},
child: const Text('詳細を見る'),
),
),
SizedBox は空白専用ではなく、子に与える幅や高さを決める小さな箱でもあります。ボタンのサイズをここへ寄せると、余白と装飾を分けたまま操作部品の大きさだけを制御できます。
4. Padding と margin を分けて考える
余白の話で最初に混ざりやすいのが、内側と外側です。
Padding: 子を内側へ押し込む余白margin: 箱ごと周囲から離す余白
Padding は専用 Widget として独立しています。一方、margin は Container のプロパティです。名前は別ですが、最初は内側か外側かだけ見れば十分です。
flowchart LR
Outside[周囲の空き] --> Margin[margin]
Margin --> Box[Container本体]
Box --> Padding[padding]
Padding --> Child[子Widget]
lib/main.dart の最小例です。
このファイルは、margin と Padding がどちらも余白に見えても、効く場所が違うことを見比べるための最小例です。箱の外側を空ける例と、箱の内側へ文字を押し込む例を並べています。
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('Padding vs margin')),
body: ListView(
children: [
Container(
margin: const EdgeInsets.all(16),
color: Colors.orange.shade100,
child: const Text('margin だけを付けた箱'),
),
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
color: Colors.green.shade100,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('padding を付けると、文字が箱の内側へ下がる'),
),
),
Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueGrey),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text('外側の距離と内側の距離を分けて書ける'),
),
),
],
),
),
);
}
}
コードのポイント
① margin は箱ごと周囲から離す
Container(
margin: const EdgeInsets.all(16),
color: Colors.orange.shade100,
child: const Text('margin だけを付けた箱'),
),
margin はコンテナ自身の外側へ効くため、周囲の部品や画面端との距離を表すときに使います。フォームやカード全体を少し離したい場面では、まずこちらを疑うと整理しやすくなります。
② Padding は子を箱の内側へ押し込む
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
color: Colors.green.shade100,
child: const Padding(
padding: EdgeInsets.all(16),
child: Text('padding を付けると、文字が箱の内側へ下がる'),
),
),
Padding は子 Widget の周囲に内側余白を作る役割です。外側の距離を作る margin と分けておくと、あとで余白だけを直したいときに触る場所を迷わず済みます。
③ カード UI では gap・内側余白・外側余白を分担させる
Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueGrey),
),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Text('外側の距離と内側の距離を分けて書ける'),
),
),
同じ箱の中に margin と Padding を並べると、外側と内側の役割差がはっきり見えます。部品同士の距離は SizedBox、カード内部は Padding、カード全体と周囲の距離は margin と分けると、修正箇所を追いやすくなります。
5. Container は装飾と配置をまとめる箱
Container が便利なのは、サイズ、余白、背景色、枠線、角丸、子の配置を 1 か所でまとめられるからです。逆に言うと、そうした要素が必要な場面に絞ると、なぜ Container を置いたか追いやすくなります。
次の lib/main.dart では、業務画面の小さなステータスカードを作ります。
このファイルは、Container を「装飾と配置をまとめる箱」として使う典型例です。カード全体の見た目と、小さなステータスラベルの見た目を別々の Container で担当させています。
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('Container sample')),
backgroundColor: const Color(0xFFF5F7FA),
body: Center(
child: Container(
width: 300,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFD6DCE5)),
boxShadow: const [
BoxShadow(
color: Color(0x14000000),
blurRadius: 16,
offset: Offset(0, 8),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(999),
),
child: Text(
'同期済み',
style: TextStyle(
color: Colors.green.shade800,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 12),
const Text(
'最終送信ジョブ',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text('伝票番号: SH-2026-0315'),
const SizedBox(height: 4),
const Text('処理件数: 24 件'),
],
),
),
),
),
);
}
}
コードのポイント
① 外側の Container はカード全体の見た目をまとめて持つ
child: Container(
width: 300,
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFD6DCE5)),
この Container にはサイズ、内外余白、配置、背景、角丸、枠線が集まっています。こうした箱全体の見た目をまとめて持たせたいときは、Padding や SizedBox より Container の責務が素直です。
② 内側の Container はステータスラベルの装飾だけを担当する
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(999),
),
内側の小さい Container は、ラベルへ背景色と角丸を付けるためだけに存在しています。同じ Container でも、どの見た目をどこで持つかを分けると、あとでカード全体とラベルだけを別々に調整しやすくなります。
6. 不要な Container を減らす
Container を減らしたい理由は、パフォーマンスより先に可読性です。何をしたいのかが小さな Widget 名で読めるほうが、修正箇所を追いやすくなります。
判断基準は次の表で十分です。
| やりたいこと | まず選ぶ Widget | 理由 |
|---|---|---|
| タイトルと本文の間を 12px 空けたい | SizedBox | gap だとすぐ分かるため |
| カードの内側へ 16px の余白を入れたい | Padding | 子を内側へ押し込む目的だから |
| カード全体へ背景色、角丸、枠線を付けたい | Container | 箱の見た目をまとめて持たせるため |
| 子を中央へ寄せたいだけ | Center または Align | 配置だけなら専用 Widget のほうが意図が明確だから |
| 背景色を付けたいだけ | ColoredBox | 色だけが目的なら責務が小さいから |
比較用の lib/main.dart です。
このファイルは、Container を何でも箱にする書き方と、役割ごとに 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('Avoid overusing Container')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'Container を何でも箱に入れた例',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(16),
color: Colors.red.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('出荷ステータス'),
Container(height: 12),
Container(
padding: const EdgeInsets.all(8),
color: Colors.white,
child: const Text('処理待ち 3 件'),
),
],
),
),
const SizedBox(height: 24),
const Text(
'役割ごとに分けた例',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
ColoredBox(
color: Colors.green.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('出荷ステータス'),
const SizedBox(height: 12),
const ColoredBox(
color: Colors.white,
child: Padding(
padding: EdgeInsets.all(8),
child: Text('処理待ち 3 件'),
),
),
],
),
),
),
],
),
),
);
}
}
コードのポイント
① 余白のためだけに Container を置くと責務が見えにくくなる
Container(
padding: const EdgeInsets.all(16),
color: Colors.red.shade50,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('出荷ステータス'),
Container(height: 12),
Container(
前半の例では、gap のために Container(height: 12) まで登場しており、どこが余白でどこが装飾かを一目で追いにくくなっています。背景や枠線を持たない空白なら、より責務の小さい Widget へ寄せたほうが意図が明確です。
② 役割ごとに ColoredBox・Padding・SizedBox へ分けると読みやすい
ColoredBox(
color: Colors.green.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('出荷ステータス'),
const SizedBox(height: 12),
後半は「色を付ける」「内側へ寄せる」「距離を空ける」を別々の Widget 名で表しています。背景色と角丸と位置調整を一度に持たせたい場面なら Container で構いませんが、責務を分けられるなら分けたほうが修正箇所を追いやすくなります。
7. 迷ったときの使い分け
よくある場面を短く戻すと、判断基準は次のようになります。
| 場面 | 選ぶもの | 見る理由 |
|---|---|---|
| タイトルと本文の間を 12px 空ける | SizedBox(height: 12) | 距離そのものを表しているから |
| アイコンとテキストの間を 8px 空ける | SizedBox(width: 8) | 横方向の gap だと分かるから |
| カードの中身を 16px だけ内側へ寄せる | Padding | 内側余白の責務だから |
| カード全体を画面端から 16px 離す | margin | 箱ごとの外側余白だから |
| カードへ背景色、枠線、角丸を付ける | Container | 箱の見た目をまとめて持つため |
迷ったら、次の順で見ると整理しやすくなります。
- 今やりたいのは距離か、内側余白か、箱の見た目か
- gap だけなら
SizedBoxで足りないか - 子の周囲だけなら
Paddingで足りないか - それでも背景、枠線、角丸、配置が必要なら
Containerを使う
8. まとめ
SizedBox は間隔と固定サイズ、Padding は内側余白、Container は箱の見た目と配置をまとめる Widget です。この 3 つを役割で分けるだけでも、Flutter の UI コードで各部品の責務を追いやすくなります。
最初の 1 画面では、gap のためだけに Container を足しがちです。そこを SizedBox と Padding へ分けるだけでも、どこを直せばよいかが見えやすくなります。次に一覧やフォームへ進むときも、この分け方がそのまま効きます。