【Flutter】GridViewでインスタグラム風UIを簡単作成【サンプルコード】

こんにちはyu_to(@yu_to_python)です。

今回はFlutterで写真等を画面いっぱいに並べてスクロールさせたい時などに使えるGridViewというWidgetについて解説していきます。

そもそもWidgetの事について詳しく知らないという方はこちらの記事で詳しく解説しているので参考にしてください。

【Flutter】UI構築する際の基本Widgetついて解説!【サンプルコードあり】

この記事を最後まで読めばGridViewの使い方、よく使うプロパティについて理解できると思うので是非最後まで読んでみてください。

GridViewの特徴

写真アプリの様なUIを簡単に作れる

GridViewを使えばインスタグラム、iPhoneの写真アプリやGoogle Photo、Pinterestの様な写真等を画面いっぱいに並べてスクロールさせるアプリUIを簡単に作成できます。

表示させたい要素の並べ方のオプションも多く用意されているので、自分の思った通りに要素を表示させることが可能です。

4種類の表示方法

GridViewには4種類のコンストラクタ(表示方法)が用意されています。

  1. GridView.count(横 or 縦に並べる数を予め決めて表示)
  2. GridView.extent(横 or 縦に並べるWidgetの幅を予め決めて表示)
  3. GridView.builder(動的に表示させるWidget数を変更しながら表示)
  4. GridView.custom(自分で好きなようにGridViewをカスタマイズして表示)

基本アプリを開発する上では1~3までのコンストラクタを抑えておけば困ることはありません。

GridView.customは更に自分でカスタマイズして柔軟にWidgetを並べることができるようになりますが、僕自身一度も使ったことが無いですしほぼ使用する機会は無いでしょう。

今回はGridView.custom以外の3つの表示方法について解説していきます。

サンプルコード

コピペですぐ動かせるのでまずは実際に動かしてみてください。

動作確認したバージョンは下記の通りです。

  • Flutter 3.0.1
  • Dart 2.13.3
main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'demo',
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({Key key}) : super(key: key);
  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  int selectedIndex = 0;
  List<Widget> display = [Count(), Extent(), Builder()];

  List<String> displayAppBarText = [
    'GridView.count',
    'GridView.extent',
    'GridView.builder'
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(
          displayAppBarText[selectedIndex],
        ),
      ),
      body: display[selectedIndex],
      bottomNavigationBar: BottomNavigationBar(
        items: [
          BottomNavigationBarItem(icon: Icon(Icons.photo), label: 'count'),
          BottomNavigationBarItem(icon: Icon(Icons.photo), label: 'extent'),
          BottomNavigationBarItem(icon: Icon(Icons.photo), label: 'builder'),
        ],
        currentIndex: selectedIndex,
        onTap: (int index) {
          selectedIndex = index;
          setState(() {});
        },
      ),
    );
  }
}

// GridView.countを使った画面
class Count extends StatelessWidget {
  const Count({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return GridView.count(
      // 横1行あたりに表示するWidgetの数
      crossAxisCount: 4,
      // Widget間のスペース(左右)
      crossAxisSpacing: 4,
      // Widget間のスペース(上下)
      mainAxisSpacing: 4,
      // 全体の余白
      padding: const EdgeInsets.all(4),
      children: [
        // スプレッド演算子を使って画像を100個生成
        for (int i = 0; i < 100; i++) ...[
          const Image(
            image: NetworkImage(
                'https://gakogako.com/wp-content/uploads/2021/01/2B645D22-ACA1-4F76-811A-30C259BABE75_1_105_c-2.jpeg'),
            fit: BoxFit.cover,
          )
        ],
      ],
    );
  }
}

// GridView.extentを使った画面
class Extent extends StatelessWidget {
  const Extent({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return Center(
      child: SizedBox(
        // GridViewの表示幅を固定
        width: 300,
        child: GridView.extent(
          // maxCrossAxisExtent : 表示させたいWidget1つ辺りの最大横幅を指定
          // GridViewの表示幅 / maxCrossAxisExtent で画面幅内に表示されるWidgetの数が決まる
          // 300 / 50 = 6
          // → 画面横に6つWidgetが並ぶ
          maxCrossAxisExtent: 50,
          // 全体の余白
          padding: const EdgeInsets.all(4),
          // Widget間のスペース(左右)
          mainAxisSpacing: 4,
          // Widget間のスペース(上下)
          crossAxisSpacing: 4,
          children: [
            // スプレッド演算子を使って画像を100個生成
            for (int i = 0; i < 100; i++) ...[
              const Image(
                image: NetworkImage(
                    'https://gakogako.com/wp-content/uploads/2021/01/2B645D22-ACA1-4F76-811A-30C259BABE75_1_105_c-2.jpeg'),
                fit: BoxFit.cover,
              )
            ],
          ],
        ),
      ),
    );
  }
}

// GridView.builderを使った画面
class Builder extends StatelessWidget {
  const Builder({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return GridView.builder(
        // Widgetを何個表示させるかを指定。指定しなければ無限に表示
        // itemCount: 100,
      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
        // 横1行あたりに表示するWidgetの数
        crossAxisCount: 2,
        // Widget間のスペース(左右)
        mainAxisSpacing: 4,
        // Widget間のスペース(上下)
        crossAxisSpacing: 4,
      ),
      // 全体の余白
      padding: const EdgeInsets.all(4),
      itemBuilder: (BuildContext context, int index) {
        return const Image(
          image: NetworkImage(
              'https://gakogako.com/wp-content/uploads/2021/01/2B645D22-ACA1-4F76-811A-30C259BABE75_1_105_c-2.jpeg'),
          fit: BoxFit.cover,
        );
      },
    );
  }
}

ビルドした時の動きはこんな感じです。

この様に僕の実家で飼ってる猫さん画像が写真アプリの様に画面いっぱいにスクロールされて表示できているのが分かると思います。

このサンプルアプリ内で使われているフッターを使った画面切り替えの処理とスプレッド演算子(…)の処理がよく分からない人は↓の記事で詳しく解説しているのでよければ参考にしてみてください。

【Flutter/Dart】スプレッド演算子(…)を使ってWidgetを動的に自動生成する

【BottomNavigationBar】フッターで画面切り替えする方法を徹底解説【Flutter】

 

よく使うプロパティ

crossAxisCount

横1行あたりに表示するWidgetの数を設定するプロパティです。

この後紹介するScrollDirectionプロパティでAxis.horizontalを設定していると縦1行あたりに表示するWidgetの数を設定になります。

countとbuilderのコンストラクタでは必須のプロパティになってます。

crossAxisCount
...
return GridView.count(
  // 横1行あたりに表示するWidgetの数
  crossAxisCount: 4,
...

maxCrossAxisExtent

表示させたいWidget1つ辺りの最大横幅を指定するプロパティです。

例えばGridViewの表示領域が300pxだったとして、そこにmaxCrossAxisExtentを50と指定すると、画面の横に6個Widgetが並ぶことになります。

extentのコンストラクタでは必須のプロパティになってます。

maxCrossAxisExtent
...
child: GridView.extent(
  // maxCrossAxisExtent : 表示させたいWidget1つ辺りの最大横幅を指定
  // GridViewの表示幅 / maxCrossAxisExtent で画面幅内に表示されるWidgetの数が決まる
  // 300 / 50 = 6
  // → 画面横に6つWidgetが並ぶ
  maxCrossAxisExtent: 50,
...

padding

GridView全体のパディングを設定できます。

count, extent, builder全てのコンストラクタで使用可能です。

padding
...
child: GridView.extent(
  // 全体の余白
  padding: const EdgeInsets.all(4),
...

mainAxisSpacing / crossAxisSpacing

表示させてるWidget間のスペースを設定できるプロパティです。

count, extent, builder全てのコンストラクタで使用可能です。

mainAxisSpacing / crossAxisSpacing
...
child: GridView.extent(
  // Widget間のスペース(左右)
  mainAxisSpacing: 4,
  // Widget間のスペース(上下)
  crossAxisSpacing: 4,
...

ScrollDirection

Widgetを並べる向きを横に設定できるプロパティです。

デフォルトだとAxis.verticalに設定されていて縦にWidgetが表示されるようになっています。

Axis.horizontalを指定すると横にWidgetが表示されます。その際スクロールも横に移動するようになります。

count, extent, builder全てのコンストラクタで使用可能です。

ScrollDirection
...
return GridView.count(
  // Widgetを横に表示させる
  scrollDirection: Axis.horizontal,
...

itemCount

Widgetをいくつ表示させるかを設定できるプロパティです。

設定しなければ無限に表示されることになります。

builderのみ使用可能です。

itemCount
...
return GridView.builder(
    // Widgetを何個表示させるかを指定。指定しなければ無限に表示
    // itemCount: 100,
...

tips

表示させたWidgetをタップできるようにしたい

表示させた個別のWidgetをタップした時に何か処理を実行したい時はそのWidgetをGestureDetectorでラップしてonTapプロパティを設定してあげると実装できます。

main.dart
...
return GridView.count(
  crossAxisCount: 4,
  crossAxisSpacing: 4,
  mainAxisSpacing: 4,
  padding: const EdgeInsets.all(4),
  children: [
    for (int i = 0; i < 100; i++) ...[
      GestureDetector(
        onTap: () => print('$iつめの猫がTapされた!'),
        child: const Image(
          image: NetworkImage(
              'https://gakogako.com/wp-content/uploads/2021/01/2B645D22-ACA1-4F76-811A-30C259BABE75_1_105_c-2.jpeg'),
          fit: BoxFit.cover,
        ),
      )
    ],
  ],
);
...

応用編:GitHubのcontributionsグラフの様なUI

GridViewを使うと画面スクロールする系のUI以外にも同じWidgetを何個も並べて作る様なUIも作れます。

例えばエンジニアにはお馴染みGitHubのcontributionsグラフの様に同じ四角形がいくつも並んだ様なUIもGridViewで実現できます。

サンプルコードも貼っておくので良ければ参考にしてみてください。

main.dart
import 'package:flutter/material.dart';
import 'dart:math' as math;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'demo',
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  const Home({Key key}) : super(key: key);
  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  // 日にちごとのコミット回数を格納する用
  List<int> commit;
  // 画面にどれくらいWidgetを表示するか(7の倍数)
  int generateWidgetCount = 210;
  @override
  void initState() {
    // 現在の日付データを取得
    DateTime dt = DateTime.now();
    // dt.weekday : 曜日を数字で取得できる。(月:1, 火:2, 水:3, 木:4, 金:5, 土:6, 日:7)
    generateWidgetCount += (dt.weekday + 1);

    commit = [
      // 整数をランダム生成
      for (int i = 0; i < generateWidgetCount; i++) ...[
        math.Random().nextInt(10)
      ]
    ];
    // ↓commitの中身
    // flutter: [3, 2, 4, 2, 0, 3, 1, ...

    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'GitHubのcontributionsグラフUI',
        ),
      ),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(8.0),
          child: SizedBox(
            height: 130,
            child: Row(
              children: [
                Expanded(
                  flex: 1,
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    // 曜日
                    children: [
                      Spacer(flex: 3),
                      Text('Mon'),
                      Spacer(flex: 2),
                      Text('Wed'),
                      Spacer(flex: 2),
                      Text('Fri'),
                      Spacer(flex: 3),
                    ],
                  ),
                ),
                Expanded(
                  flex: 11,
                  child: Center(
                    child: GridView.builder(
                      // commit内のリスト数を設定
                      itemCount: commit.length,
                      // GridViewをCenterでラップしてshrinkWrapをtrueにすると中央寄せにできる
                      shrinkWrap: true,
                      // Widgetを横に表示させる
                      scrollDirection: Axis.horizontal,
                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                        // 横1行あたりに表示するWidgetの数
                        crossAxisCount: 7,
                        // Widget間のスペース(左右)
                        mainAxisSpacing: 4,
                        // Widget間のスペース(上下)
                        crossAxisSpacing: 4,
                      ),
                      // GridView全体の余白
                      padding: const EdgeInsets.all(8),
                      itemBuilder: (BuildContext context, int i) {
                        return Container(
                          decoration: BoxDecoration(
                            // 即時関数で色の濃淡を設定
                            color: (() {
                              if (commit[i] == 0) {
                                return Colors.grey[300];
                              } else if (commit[i] <= 2) {
                                return Colors.green[200];
                              } else if (commit[i] <= 4) {
                                return Colors.green[400];
                              } else if (commit[i] <= 6) {
                                return Colors.green[600];
                              } else {
                                return Colors.green[800];
                              }
                            })(),
                            borderRadius: BorderRadius.circular(2),
                          ),
                        );
                      },
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

おわり

今回はGridViewについて解説してきました。

今回紹介した仕様やプロパティ以外も、もっと深堀りしたい方は公式ドキュメントを参照してみてください。

このブログでは他にもFlutterに関する記事を上げているので良ければそちらも参考にしてみてください。

質問やご指摘もお待ちしてますー。

【2022年最新】現役エンジニアがFlutter初学者向けおすすめ技術書3選を紹介!

 

【Flutter】ElevatedButtonでイケてるボタンのレイアウトを簡単作成【サンプルコードあり】

【Flutter】Widgetのサイズを変更できるSizedBoxの使い方【サンプルコードあり】