はじめに
Flutter では、Widget を組み合わせてツリー構造を作ることで UI を構築します。HTML/CSS のような「テンプレート + スタイルシート」方式ではなく、すべてが Widget です。テキストもボタンもパディングもレイアウトもすべて Widget であり、それらを入れ子にしていくことで画面を作ります。
この記事では、Flutter でレイアウトを組む際に最も頻繁に使う Widget を順番に解説します。
1. Column ― 縦方向に Widget を並べる
Column は子 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('Column デモ')),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(width: 100, height: 50, color: Colors.red),
Container(width: 150, height: 50, color: Colors.green),
Container(width: 200, height: 50, color: Colors.blue),
],
),
),
);
}
}
mainAxisAlignment(主軸方向の配置)
Column の主軸は縦方向です。mainAxisAlignment で子 Widget の縦方向の配置を制御します。
| 値 | 説明 |
|---|---|
MainAxisAlignment.start |
上詰め(デフォルト) |
MainAxisAlignment.center |
中央揃え |
MainAxisAlignment.end |
下詰め |
MainAxisAlignment.spaceBetween |
最初と最後の Widget を端に置き、残りを均等配置 |
MainAxisAlignment.spaceEvenly |
すべての間隔を均等にする |
MainAxisAlignment.spaceAround |
各 Widget の周囲に均等なスペースを置く(端は半分) |
crossAxisAlignment(交差軸方向の配置)
Column の交差軸は横方向です。crossAxisAlignment で子 Widget の横方向の配置を制御します。
| 値 | 説明 |
|---|---|
CrossAxisAlignment.center |
中央揃え(デフォルト) |
CrossAxisAlignment.start |
左揃え |
CrossAxisAlignment.end |
右揃え |
CrossAxisAlignment.stretch |
横幅いっぱいに引き伸ばす |
2. Row ― 横方向に Widget を並べる
Row は子 Widget を**横方向(左から右)**に並べます。Column と同じ mainAxisAlignment / crossAxisAlignment プロパティを持ちますが、軸の方向が逆になります。
- Row の主軸は横方向、交差軸は縦方向
- Column の主軸は縦方向、交差軸は横方向
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 デモ')),
body: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(width: 80, height: 80, color: Colors.red),
Container(width: 80, height: 120, color: Colors.green),
Container(width: 80, height: 60, color: Colors.blue),
],
),
),
);
}
}
覚え方: Row は「行(横一列)」、Column は「列(縦一列)」。mainAxis は Widget が並ぶ方向、crossAxis はそれと直交する方向です。
3. Container ― 装飾・サイズ・余白を指定する万能 Widget
Container は単一の子 Widget に対して、サイズ・色・余白・装飾などを設定できます。
基本プロパティ
Container(
width: 200,
height: 100,
color: Colors.blue, // 背景色(decoration と併用不可)
padding: EdgeInsets.all(16), // 内側の余白
margin: EdgeInsets.all(8), // 外側の余白
child: Text('Hello'),
)
EdgeInsets のバリエーション
| コンストラクタ | 説明 |
|---|---|
EdgeInsets.all(16) |
上下左右すべて 16 |
EdgeInsets.symmetric(horizontal: 16, vertical: 8) |
横 16、縦 8 |
EdgeInsets.only(left: 10, top: 20) |
指定した辺だけ |
EdgeInsets.fromLTRB(10, 20, 10, 20) |
左・上・右・下の順 |
decoration(BoxDecoration)
decoration を使うと、角丸・枠線・影・グラデーションなど高度な装飾ができます。color プロパティと decoration は同時に使えません。色は BoxDecoration 内の color に指定してください。
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 decoration')),
body: Center(
child: Container(
width: 250,
height: 150,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 8,
offset: const Offset(2, 4),
),
],
gradient: const LinearGradient(
colors: [Colors.lightBlue, Colors.blueAccent],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: Text(
'カード風デザイン',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
),
),
);
}
}
4. Expanded / Flexible ― 残りスペースの分配
Row や Column の中で、余ったスペースを子 Widget に分配するには Expanded または Flexible を使います。
Expanded
Expanded は残りのスペースをすべて埋めるように子 Widget を拡張します。flex プロパティで比率を指定できます(デフォルトは 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('Expanded デモ')),
body: Row(
children: [
Expanded(
flex: 2, // 2/4 = 50%
child: Container(height: 100, color: Colors.red),
),
Expanded(
flex: 1, // 1/4 = 25%
child: Container(height: 100, color: Colors.green),
),
Expanded(
flex: 1, // 1/4 = 25%
child: Container(height: 100, color: Colors.blue),
),
],
),
),
);
}
}
上記の場合、flex の合計は 2 + 1 + 1 = 4 です。赤は 2/4 = 50%、緑と青はそれぞれ 1/4 = 25% の幅になります。
Flexible
Flexible は Expanded と似ていますが、fit プロパティで挙動を制御できます。
-
FlexFit.tight(Expanded と同じ): 残りスペースを埋める -
FlexFit.loose(Flexible のデフォルト): 子 Widget が必要とするサイズまでしか使わない
5. Stack / Positioned ― 重ね合わせレイアウト
Stack は子 Widget を重ねて配置します。CSS の position: absolute に相当する概念です。
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 デモ')),
body: Center(
child: SizedBox(
width: 300,
height: 300,
child: Stack(
alignment: Alignment.center,
children: [
// 一番下のレイヤー
Container(width: 300, height: 300, color: Colors.red),
// 中間レイヤー
Container(width: 200, height: 200, color: Colors.green),
// 一番上のレイヤー
Container(width: 100, height: 100, color: Colors.blue),
// Positioned で自由に配置
const Positioned(
top: 10,
right: 10,
child: Icon(Icons.star, color: Colors.yellow, size: 40),
),
const Positioned(
bottom: 10,
left: 10,
child: Text(
'左下',
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
],
),
),
),
),
);
}
}
-
alignment: Positioned でない子 Widget の配置を指定(デフォルトはAlignment.topLeft) -
Positioned:top,left,right,bottomで親 Stack の端からの距離を指定
6. SizedBox ― 固定サイズのスペーサー
SizedBox は固定の幅・高さを持つボックスです。Widget 間のスペースとしてよく使います。
Column(
children: [
Container(width: 100, height: 50, color: Colors.red),
const SizedBox(height: 20), // 縦方向に 20px のスペース
Container(width: 100, height: 50, color: Colors.blue),
],
)
Row 内では SizedBox(width: 20) のように横方向のスペースとして使えます。
7. Wrap ― 折り返しレイアウト
Wrap は子 Widget が親の幅(または高さ)に収まらない場合に自動で折り返します。タグ一覧やチップの表示に便利です。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final tags = ['Flutter', 'Dart', 'Widget', 'レイアウト', 'UI', 'モバイル', '入門'];
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Wrap デモ')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Wrap(
spacing: 8, // 横方向のスペース
runSpacing: 8, // 行間のスペース
children: tags.map((tag) {
return Chip(
label: Text(tag),
backgroundColor: Colors.blue[100],
);
}).toList(),
),
),
),
);
}
}
8. ListView ― スクロール可能なリスト
ListView はスクロール可能なリストを作成します。子 Widget の数が多い場合は ListView.builder を使うと効率的です。
ListView(少量データ向け)
ListView(
children: [
ListTile(title: Text('アイテム 1')),
ListTile(title: Text('アイテム 2')),
ListTile(title: Text('アイテム 3')),
],
)
ListView.builder(大量データ向け)
ListView.builder は表示される部分だけを遅延生成するため、大量のデータを効率的に表示できます。
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('ListView.builder デモ')),
body: ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text('アイテム ${index + 1}'),
subtitle: Text('これは ${index + 1} 番目の要素です'),
);
},
),
),
);
}
}
itemCount は要素の総数です。itemBuilder は各要素を生成するコールバックで、index は 0 から始まります。上記の場合、表示されるテキストは「アイテム 1」〜「アイテム 100」です。
9. SingleChildScrollView ― 画面全体をスクロール可能にする
Column のコンテンツが画面に収まらない場合、SingleChildScrollView で囲むとスクロール可能になります。
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('SingleChildScrollView デモ')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: List.generate(20, (index) {
return Container(
width: double.infinity,
height: 80,
margin: const EdgeInsets.only(bottom: 8),
color: Colors.primaries[index % Colors.primaries.length],
child: Center(
child: Text(
'ボックス ${index + 1}',
style: const TextStyle(color: Colors.white, fontSize: 18),
),
),
);
}),
),
),
),
);
}
}
注意:
Column+SingleChildScrollViewの場合、Column 内でExpandedは使えません。スクロール領域にはサイズの制約がないため、「残りスペース」という概念が成立しないからです。
10. 実践例 ― SNS風プロフィール画面
ここまで学んだ Widget を組み合わせて、SNS 風のプロフィール画面を作ってみましょう。
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(
theme: ThemeData(useMaterial3: true),
home: const ProfilePage(),
);
}
}
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('プロフィール')),
body: SingleChildScrollView(
child: Column(
children: [
// ヘッダー部分(カバー画像 + アバター)
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: double.infinity,
height: 180,
color: Colors.blueAccent,
child: const Center(
child: Text(
'カバー画像エリア',
style: TextStyle(color: Colors.white70, fontSize: 16),
),
),
),
const Positioned(
bottom: -40,
left: 16,
child: CircleAvatar(
radius: 45,
backgroundColor: Colors.white,
child: CircleAvatar(
radius: 42,
backgroundColor: Colors.grey,
child: Icon(Icons.person, size: 50, color: Colors.white),
),
),
),
],
),
const SizedBox(height: 50),
// ユーザー情報
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Flutter太郎',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const Text(
'@flutter_taro',
style: TextStyle(color: Colors.grey, fontSize: 14),
),
const SizedBox(height: 8),
const Text('Flutter と Dart が大好きなエンジニアです。毎日コードを書いています。'),
const SizedBox(height: 12),
// フォロー数
Row(
children: [
_buildStatItem('120', 'フォロー中'),
const SizedBox(width: 24),
_buildStatItem('4,500', 'フォロワー'),
const SizedBox(width: 24),
_buildStatItem('350', '投稿'),
],
),
const SizedBox(height: 16),
// タグ
Wrap(
spacing: 8,
runSpacing: 4,
children: ['Flutter', 'Dart', 'Firebase', 'UI/UX']
.map((tag) => Chip(label: Text(tag)))
.toList(),
),
const SizedBox(height: 16),
// 投稿一覧(ListView は Column 内で使う場合に shrinkWrap が必要)
const Text(
'最近の投稿',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 5,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'投稿タイトル ${index + 1}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
'これは ${index + 1} 番目の投稿の本文です。Flutter でアプリ開発中...',
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.favorite_border, size: 16),
const SizedBox(width: 4),
Text('${(index + 1) * 12}'),
const SizedBox(width: 16),
const Icon(Icons.comment_outlined, size: 16),
const SizedBox(width: 4),
Text('${(index + 1) * 3}'),
],
),
],
),
),
);
},
),
],
),
),
],
),
),
);
}
static Widget _buildStatItem(String count, String label) {
return Row(
children: [
Text(count, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 4),
Text(label, style: const TextStyle(color: Colors.grey)),
],
);
}
}
このコードでは以下の Widget を活用しています。
| Widget | 用途 |
|---|---|
Stack + Positioned
|
カバー画像の上にアバターを重ねる |
Column |
画面全体を縦方向に構成 |
Row |
フォロー数を横に並べる |
Wrap |
タグを折り返して表示 |
ListView.builder |
投稿一覧のリスト |
SingleChildScrollView |
画面全体のスクロール |
Container / SizedBox
|
サイズとスペースの調整 |
ポイント:
Columnの中にListViewを置く場合は、shrinkWrap: trueとphysics: NeverScrollableScrollPhysics()を設定します。そうしないとスクロール領域の高さが無限になりエラーになります。
練習問題
問題 1
以下の要件を満たすカードレイアウトを作成してください。
- 横幅いっぱいのカード(角丸 12、影付き)
- カード内に Row でアイコン(
Icons.shopping_cart)とテキスト(「買い物リスト」)を横並びにする - アイコンとテキストの間は 12px のスペース
- カード内の padding は上下左右 16px
模範解答
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('練習問題 1')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: const Row(
children: [
Icon(Icons.shopping_cart, size: 28, color: Colors.blue),
SizedBox(width: 12),
Text(
'買い物リスト',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
],
),
),
),
),
);
}
}
問題 2
Row と Expanded を使って、以下のような 3 カラムレイアウトを作成してください。
- 左カラム: 青色、flex: 1
- 中央カラム: 赤色、flex: 2
- 右カラム: 緑色、flex: 1
- 各カラムの高さは 200
- カラム間のスペースは 8px(
SizedBoxを使う)
模範解答
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('練習問題 2')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
flex: 1,
child: Container(height: 200, color: Colors.blue),
),
const SizedBox(width: 8),
Expanded(
flex: 2,
child: Container(height: 200, color: Colors.red),
),
const SizedBox(width: 8),
Expanded(
flex: 1,
child: Container(height: 200, color: Colors.green),
),
],
),
),
),
);
}
}
SizedBox は Expanded ではないため、固定の 8px 幅を確保します。残りのスペースが 1:2:1 の比率で 3 つの Expanded に分配されます。
まとめ
| Widget | 用途 |
|---|---|
Column |
縦方向に並べる |
Row |
横方向に並べる |
Container |
サイズ・装飾・余白を設定 |
Expanded / Flexible
|
残りスペースを分配 |
Stack / Positioned
|
重ね合わせ |
SizedBox |
固定サイズのスペーサー |
Wrap |
折り返しレイアウト |
ListView / ListView.builder
|
スクロール可能なリスト |
SingleChildScrollView |
画面全体をスクロール可能にする |
これらの Widget を組み合わせれば、ほとんどのレイアウトを実現できます。次回は StatefulWidget と状態管理の基本を学び、ユーザー操作に応じて画面が変化するアプリを作ります。
参考
- Flutter公式ドキュメント - Layouts in Flutter
- Flutter公式ドキュメント - Column class
- Flutter公式ドキュメント - Row class
- Flutter公式ドキュメント - Container class
- Flutter公式ドキュメント - Stack class
- Flutter公式ドキュメント - ListView class
- Flutter公式ドキュメント - Expanded class
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!