公開日 2026-07-01

FlutterのListViewとGridViewで一覧画面を作る(基本パターン)

Flutter の ListView・ListView.builder・GridView.count を最小例で整理し、一覧画面を縦リストかグリッドで組み始める判断基準を持てるようにする。

目次

  1. 1. ゴールと非対象
  2. 対象読者
  3. この記事で到達する状態
  4. 非対象
  5. 2. 先に一覧の種類を分ける
  6. 3. ListView は 1 列の一覧を作る
  7. コードのポイント
  8. 4. ListView.builder で繰り返し描画する
  9. コードのポイント
  10. コードのポイント
  11. 5. GridView は複数列の一覧を作る
  12. コードのポイント
  13. 6. よくある詰まりどころを先に避ける
  14. 7. 迷ったときの使い分け
  15. 8. まとめ

FlutterのContainerとSizedBoxを使いこなす(余白・サイズ・装飾の基本) の次に、一覧画面を作ろうとして最初に迷いやすいのが ListViewGridView の選び分けです。止まりやすいのは Widget 名そのものより、「縦に流すべきか」「カードを面で並べるべきか」「builder はいつ使うのか」が曖昧なまま書き始めるところにあります。この記事では、Flutter 初学者向けに ListView ListView.builder GridView.count の基本パターンを整理し、最初の一覧画面を自力で組み始める基準をまとめます。

1. ゴールと非対象

対象読者

  • Flutter の環境構築、Dart 入門、Column / Row / Stack の基本までは終わった人
  • ContainerSizedBox の役割分担は分かったが、一覧画面を何で組むかまだ曖昧な人
  • ListViewGridView を見たことはあるが、どちらを選ぶか判断基準がない人

この記事で到達する状態

  • ListView(children: ...) で固定項目の一覧を作れる
  • ListView.builder でデータから繰り返し行を描画できる
  • scrollDirection で縦スクロールと横スクロールを切り替えられる
  • GridView.count で 2 列のカード一覧を作れる
  • ListViewGridView の使い分けを説明できる

非対象

  • CustomScrollView / SliverList / SliverGrid
  • 無限スクロールやページング
  • REST API 取得と状態管理
  • GridView.builder の詳細比較
  • パフォーマンス最適化の深掘り

状態管理や API 取得は次の記事へ回し、ここでは一覧の形とスクロールの基本を扱います。

2. 先に一覧の種類を分ける

最初に、ListViewGridView の役割を分けます。

Widget向いている見せ方まず見るポイントよくある場面
ListView1 列で上から下へ読む一覧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]

縦一覧かグリッドかを先に決め、その後に childrenbuilder かを選ぶ—並び方と作り方は別の軸です。

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 に切り替える理由はまだありません。

ListView children サンプル画面

4. ListView.builder で繰り返し描画する

同じ見た目の行をデータから繰り返し作るなら、ListView.builder を選びます。ここでの利点は「大量件数に耐える」ことだけではありません。同じ行を何度も手で書かずに済み、後で API 通信へつなげやすくなる点にあります。

まずは縦方向の ListView.builder を確認します。この lib/main.dart は、データ件数と行の描画処理を分ける最小例です。itemCountitemBuilder の役割を切り分けて見ます。

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 は表示順とデータ参照の両方に使えます。同じカード構造を繰り返しつつ、番号やタイトルだけを差し替える流れが見やすい構成です。

ListView.builder(縦)サンプル画面

横スクロールへ切り替えるときは、同じ builderscrollDirection: 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 行の見せ方です。

ListView.builder(横)サンプル画面

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 のほうが読みやすい場面もあります。グリッドは「同じ大きさで見比べたいか」を先に見ると選びやすくなります。

GridView.count サンプル画面

6. よくある詰まりどころを先に避ける

一覧 Widget はすぐ使えますが、最初に詰まりやすい場所が 2 つあります。

  • Column の中に ListViewGridView をそのまま置くと、高さが決まらずエラーになることがある
  • 横スクロールの ListView は、高さを決めないと意図した見た目になりにくい

最初は次の 2 点だけ見れば、配置で止まりにくくなります。

  1. 画面の残り領域いっぱいに一覧を置きたいなら、Expanded(child: ListView(...)) の形にする
  2. 横スクロールのカード列を置きたいなら、SizedBox(height: 120, child: ListView.builder(...)) のように高さを先に決める

ここでは shrinkWrap や制約システムの詳細までは広げません。まずは「縦一覧には縦方向の高さが必要」「横一覧には見せたい高さが必要」という 2 点だけ押さえれば十分です。

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

よくある場面ごとに選ぶ理由を整理します。

場面選ぶもの見る理由
設定画面のメニュー一覧ListView(children: ...)項目数が少なく、行ごとに内容が少しずつ違っても書き分けやすいから
通知一覧や履歴一覧ListView.builder同じ行を件数分だけ繰り返しやすいから
画面上部の横スクロール操作カードListView.builder + Axis.horizontal1 列のまま横へ流したいから
商品カードやカテゴリ一覧GridView.count複数列で面として見比べやすいから
写真サムネイル一覧GridView.count同じ大きさの画像を並べる前提と相性がよいから

迷ったら、次の順で確認してください。

  1. 1 列で順番に読ませたいのか、面で見比べさせたいのか
  2. 項目数は少数固定か、同じ行を件数分だけ繰り返すか
  3. 縦に流すか、横へ流すか

一覧画面の最初の判断は、この 3 つを確認すれば数個の候補まで絞れます。

8. まとめ

ListView は 1 列の一覧、ListView.builder はデータから行を繰り返す一覧、GridView.count は複数列のカード一覧を作る入口です。まずは「縦に読むか、面で見比べるか」を決め、その後に childrenbuilder かを選ぶと迷いにくくなります。

次に UI 基礎を進めるなら、通知 UI の基本としてダイアログやスナックバーへ進むか、実務寄りに REST API 通信へ進むと流れがつながります。一覧で詰まったら、並び方、件数、スクロール方向の 3 つを先に確認してください。

シリーズ 12/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で複数画面を切り替える