FlutterのContainerとSizedBoxを使いこなす(余白・サイズ・装飾の基本) の次に、一覧画面を作ろうとして最初に迷いやすいのが ListView と GridView の選び分けです。止まりやすいのは Widget 名そのものより、「縦に流すべきか」「カードを面で並べるべきか」「builder はいつ使うのか」が曖昧なまま書き始めるところにあります。この記事では、Flutter 初学者向けに ListView ListView.builder GridView.count の基本パターンを整理し、最初の一覧画面を自力で組み始める基準をまとめます。
1. ゴールと非対象
対象読者
- Flutter の環境構築、Dart 入門、
Column/Row/Stackの基本までは終わった人 ContainerとSizedBoxの役割分担は分かったが、一覧画面を何で組むかまだ曖昧な人ListViewとGridViewを見たことはあるが、どちらを選ぶか判断基準がない人
この記事で到達する状態
ListView(children: ...)で固定項目の一覧を作れるListView.builderでデータから繰り返し行を描画できるscrollDirectionで縦スクロールと横スクロールを切り替えられるGridView.countで 2 列のカード一覧を作れるListViewとGridViewの使い分けを説明できる
非対象
CustomScrollView/SliverList/SliverGrid- 無限スクロールやページング
- REST API 取得と状態管理
GridView.builderの詳細比較- パフォーマンス最適化の深掘り
状態管理や API 取得は次の記事へ回し、ここでは一覧の形とスクロールの基本を扱います。
2. 先に一覧の種類を分ける
最初に、ListView と GridView の役割を分けます。
| Widget | 向いている見せ方 | まず見るポイント | よくある場面 |
|---|---|---|---|
ListView | 1 列で上から下へ読む一覧 | children / builder / scrollDirection | メニュー一覧、通知一覧、履歴一覧 |
GridView | 複数列で面として見比べる一覧 | crossAxisCount / childAspectRatio | 商品カード、写真一覧、カテゴリ一覧 |
さらに ListView には 2 つの入り口があります。
- 項目数が少なく固定されているなら
ListView(children: [...]) - 同じ見た目の行をデータから繰り返すなら
ListView.builder(...)
判断を先に図で置くと、次のようになります。
flowchart TD
A[一覧画面を作りたい] --> B{1 列で読ませるか}
B -->|はい| C{項目数は少なく固定か}
C -->|はい| D[ListView children]
C -->|いいえ| E[ListView.builder]
B -->|いいえ| F[GridView.count]
縦一覧かグリッドかを先に決め、その後に children か builder かを選ぶ—並び方と作り方は別の軸です。
3. ListView は 1 列の一覧を作る
設定画面やメニューのように、数件の固定項目を順番に並べるだけなら ListView(children: ...) から始めると読みやすくなります。行ごとに表示内容が大きく違う場面でも、そのまま書き下ろせます。
以降のコード例はすべて lib/main.dart にそのまま貼って flutter run で確認できます。外部パッケージ不要のため、DartPad でも試せます。
次の lib/main.dart は、固定項目を children に並べるだけで一覧画面になる最小例です。各行を個別に書き分けられる点に注目してください。
lib/main.dart は次の内容で作成します。
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 children sample')),
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.inventory_2_outlined),
title: Text('入荷一覧'),
subtitle: Text('本日入荷予定の伝票を確認する'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.local_shipping_outlined),
title: Text('出荷一覧'),
subtitle: Text('処理待ちの出荷指示を見る'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.sync_outlined),
title: Text('同期状況'),
subtitle: Text('未送信データと最終同期時刻を確認する'),
trailing: Icon(Icons.chevron_right),
),
],
),
),
);
}
}
コードのポイント
① 固定項目は children に順番どおり並べるだけでよい
body: ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.inventory_2_outlined),
title: Text('入荷一覧'),
subtitle: Text('本日入荷予定の伝票を確認する'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.local_shipping_outlined),
title: Text('出荷一覧'),
subtitle: Text('処理待ちの出荷指示を見る'),
trailing: Icon(Icons.chevron_right),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.sync_outlined),
title: Text('同期状況'),
subtitle: Text('未送信データと最終同期時刻を確認する'),
trailing: Icon(Icons.chevron_right),
),
],
),
件数が固定なら、children に上から並べる形が最も素直です。どの行がどこに出るかをそのまま上から読めるため、初学者が一覧の構造を掴みやすくなります。
② 各行をその場で書き分けやすい
ListTile(
leading: Icon(Icons.inventory_2_outlined),
title: Text('入荷一覧'),
subtitle: Text('本日入荷予定の伝票を確認する'),
trailing: Icon(Icons.chevron_right),
),
固定メニューでは、行ごとにアイコンや説明が少しずつ違うことが多くあります。children 直書きなら、共通化を急がずに差分をそのまま表現できます。
項目数が 3 件から 5 件程度で固定なら、この書き方で十分です。最初の段階では builder に切り替える理由はまだありません。
4. ListView.builder で繰り返し描画する
同じ見た目の行をデータから繰り返し作るなら、ListView.builder を選びます。ここでの利点は「大量件数に耐える」ことだけではありません。同じ行を何度も手で書かずに済み、後で API 通信へつなげやすくなる点にあります。
まずは縦方向の ListView.builder を確認します。この lib/main.dart は、データ件数と行の描画処理を分ける最小例です。itemCount と itemBuilder の役割を切り分けて見ます。
lib/main.dart は次の内容で作成します。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final shipments = List.generate(8, (index) => '出荷指示 #${index + 1}');
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('ListView.builder sample')),
body: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: shipments.length,
itemBuilder: (context, index) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(shipments[index]),
subtitle: const Text('棚A-12 / 処理待ち'),
trailing: const Icon(Icons.chevron_right),
),
);
},
),
),
);
}
}
コードのポイント
① 件数は itemCount、見た目は itemBuilder で分ける
body: ListView.builder(
itemCount: shipments.length,
itemBuilder: (context, index) {
何件出すかと、1 件をどう描画するかが別れているため、一覧の責務が明確です。API 結果に差し替えるときも、まず shipments の作り方だけを変えれば済みます。
② index からその行のデータを引き当てる
return Card(
child: ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(shipments[index]),
subtitle: const Text('棚A-12 / 処理待ち'),
),
);
index は表示順とデータ参照の両方に使えます。同じカード構造を繰り返しつつ、番号やタイトルだけを差し替える流れが見やすい構成です。
横スクロールへ切り替えるときは、同じ builder に scrollDirection: Axis.horizontal を足します。このときは高さを先に決める必要があります。次の lib/main.dart は、横方向カード列に必要な差分だけを足した例です。
lib/main.dart は次の内容で作成します。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final quickActions = [
'検品',
'入荷',
'出荷',
'棚卸',
'同期',
];
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Horizontal ListView.builder')),
body: Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: quickActions.length,
itemBuilder: (context, index) {
return Container(
width: 140,
margin: const EdgeInsets.only(right: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.apps_outlined, color: Colors.blue.shade700),
const SizedBox(height: 12),
Text(
quickActions[index],
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
);
},
),
),
),
),
);
}
}
コードのポイント
① 横スクロールは scrollDirection の指定で切り替える
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: quickActions.length,
縦方向の builder と構造はほぼ同じで、違うのはスクロール方向です。まずこの差分だけを見ると、ListView.builder の応用として理解しやすくなります。
② 横方向では高さを先に固定する
child: SizedBox(
height: 120,
child: ListView.builder(
横スクロールの一覧は、高さが曖昧だと親レイアウトとの関係が読みづらくなります。SizedBox で先に高さを決めておくと、カードの大きさと余白の意図が追いやすくなります。
縦の履歴一覧、横のクイックメニュー、どちらも ListView.builder で組めます。違うのは並ぶ方向と 1 行の見せ方です。
5. GridView は複数列の一覧を作る
商品カードやカテゴリ一覧のように、同じ大きさの部品を面で並べたいなら GridView を選びます。行単位で読むより、複数項目を見比べやすい形になります。
最初は GridView.count で十分です。列数を crossAxisCount で決められるため、まず 2 列で並べて見た目を確認しやすくなります。
次の lib/main.dart は、同じ大きさのカードを面で並べるための最小構成です。列数とカード間隔をどこで決めているかを見ると、ListView との違いが分かります。
lib/main.dart は次の内容で作成します。
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
final categories = [
'食品',
'飲料',
'日用品',
'医薬品',
'備品',
'資材',
];
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('GridView.count sample')),
body: GridView.count(
padding: const EdgeInsets.all(16),
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.4,
children: categories.map((category) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.green.shade100),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.category_outlined, color: Colors.green.shade700),
const SizedBox(height: 12),
Text(
category,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
);
}).toList(),
),
),
);
}
}
コードのポイント
① 列数は crossAxisCount で決める
body: GridView.count(
padding: const EdgeInsets.all(16),
crossAxisCount: 2,
GridView.count は、まず何列で見せるかを決めるところから始まります。2 列に固定すると、一覧全体の密度がすぐ見えるため、グリッドの入門例として追いやすい形です。
② 間隔と縦横比をそろえると、カードを見比べやすい
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.4,
グリッドはカード同士の見た目がそろっているほど比較しやすくなります。間隔と比率をまとめて決めると、どの情報量のカードに向いたレイアウトかを判断しやすくなります。
項目ごとの説明文が長くばらつくなら、無理に GridView へ寄せず ListView のほうが読みやすい場面もあります。グリッドは「同じ大きさで見比べたいか」を先に見ると選びやすくなります。
6. よくある詰まりどころを先に避ける
一覧 Widget はすぐ使えますが、最初に詰まりやすい場所が 2 つあります。
Columnの中にListViewやGridViewをそのまま置くと、高さが決まらずエラーになることがある- 横スクロールの
ListViewは、高さを決めないと意図した見た目になりにくい
最初は次の 2 点だけ見れば、配置で止まりにくくなります。
- 画面の残り領域いっぱいに一覧を置きたいなら、
Expanded(child: ListView(...))の形にする - 横スクロールのカード列を置きたいなら、
SizedBox(height: 120, child: ListView.builder(...))のように高さを先に決める
ここでは shrinkWrap や制約システムの詳細までは広げません。まずは「縦一覧には縦方向の高さが必要」「横一覧には見せたい高さが必要」という 2 点だけ押さえれば十分です。
7. 迷ったときの使い分け
よくある場面ごとに選ぶ理由を整理します。
| 場面 | 選ぶもの | 見る理由 |
|---|---|---|
| 設定画面のメニュー一覧 | ListView(children: ...) | 項目数が少なく、行ごとに内容が少しずつ違っても書き分けやすいから |
| 通知一覧や履歴一覧 | ListView.builder | 同じ行を件数分だけ繰り返しやすいから |
| 画面上部の横スクロール操作カード | ListView.builder + Axis.horizontal | 1 列のまま横へ流したいから |
| 商品カードやカテゴリ一覧 | GridView.count | 複数列で面として見比べやすいから |
| 写真サムネイル一覧 | GridView.count | 同じ大きさの画像を並べる前提と相性がよいから |
迷ったら、次の順で確認してください。
- 1 列で順番に読ませたいのか、面で見比べさせたいのか
- 項目数は少数固定か、同じ行を件数分だけ繰り返すか
- 縦に流すか、横へ流すか
一覧画面の最初の判断は、この 3 つを確認すれば数個の候補まで絞れます。
8. まとめ
ListView は 1 列の一覧、ListView.builder はデータから行を繰り返す一覧、GridView.count は複数列のカード一覧を作る入口です。まずは「縦に読むか、面で見比べるか」を決め、その後に children か builder かを選ぶと迷いにくくなります。
次に UI 基礎を進めるなら、通知 UI の基本としてダイアログやスナックバーへ進むか、実務寄りに REST API 通信へ進むと流れがつながります。一覧で詰まったら、並び方、件数、スクロール方向の 3 つを先に確認してください。