Flutterで大量の画像付きリストを快適にスクロールできるようにする

menu事業部 フロントエンドエンジニアの坂井田です。
今回は、Flutterで大量の画像付きリストをスムーズにスクロールできるようにするtipsをご紹介します。

背景

作成中のアプリには商品を一覧で表示する機能があるのですが、店舗によっては数千規模の商品が登録されている場合があります。
また、このアプリはモバイルネットワークでの運用も考えられるため、なるべくキャッシュを効かせた状態でストレス無く閲覧できるように実装する必要があり、この調査を行いました。

結論

  • cached_network_image パッケージで遅延読み込み
  • キャッシュを適切に設定する
  • サーバ側で画像を軽量化する

上記3つを取り入れることで、先程挙げた課題点を解決することが出来ました。
次の章で順番に解説します。

(補足)リスト表示について

Flutterでリスト表示をするには ListView ウィジェットを使用しますが、これには以下の4通りの書き方があります。

  • 1️⃣ ListView
    children に書いた部品をそのままリスト化する、一番シンプルな書き方です。
return ListView(
  children: const [
    Text("aaa"),
    Text("bbb"),
    Text("ccc"),
  ],
);
  • 2️⃣ ListView.builder
    itemBuilder に書いた部品を、itemCount で指定した個数分リスト化します。
    ウィジェットは動的にレンダリングされます。
return ListView.builder(
  itemCount: scrollItemCount,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text("Item $index"),
      onTap: () {},
    );
  },
);
  • 3️⃣ ListView.separated
    要素間に表示するウィジェットを指定する場合の書き方です。
return ListView.separated(
  itemCount: scrollItemCount,
  separatorBuilder: (context, index) {
    return const Text("区切り要素");
  },
  itemBuilder: (context, index) {
    return ListTile(
      title: Text("Item $index"),
      onTap: () {},
    );
  },
);
  • 4️⃣ ListView.custom
    ListView.builder のカスタマイズバージョンです。

1. cached_network_image パッケージで遅延読み込み

大量の項目を動的にレンダリングしたいため、まずは ListView.builder を用いてシンプルに画像を表示してみます。

※ダミー画像の表示には以下のサービスを使用しています fakeimg.pl

シンプルコード

return ListView.builder(
  itemCount: scrollItemCount,
  itemBuilder: (context, index) {
    return ListTile(
      leading: Padding(
        padding: const EdgeInsets.all(4),
        child: Image.network(
          "https://fakeimg.pl/300/?font_size=150&text=$index",
          height: 100,
        ),
      ),
      title: Text("Item $index"),
      onTap: () {},
    );
  },
);

このコードを実行すると、スクロールがカクつくことがわかります。
これは、読み込みが完了するまでは画像の部分に何も表示されないために発生するもので、読み込み中に仮のウィジェットを表示してあげることで解消します。
今回は、読み込み中の表示と取得画像の表示の切り替えに cached_network_image パッケージを使用しました!

pub.dev

書き換え後のコード

return ListView.builder(
  itemCount: scrollItemCount,
  itemBuilder: (context, index) {
    return ListTile(
      leading: Padding(
        padding: const EdgeInsets.all(4),
        child: CachedNetworkImage(
          imageUrl: "https://fakeimg.pl/300/?font_size=150&text=$index",
          placeholder: (context, url) =>
              Image.asset("assets/thumbnail.png"),
          errorWidget: (context, url, error) => const Icon(Icons.error),
        ),
      ),
      title: Text("Item $index"),
      onTap: () {},
    );
  },
);

読み込み中に表示する仮のウィジェットとして、以下の画像を assets/thumbnail.png に保存しておきます。
pubspec.yamlassets 設定も必要です)

これらの設定を行うことで、以下のようにスムーズに表示することができるようになります!

2. キャッシュを適切に設定する

デフォルトの設定では、キャッシュは最大200枚、最終利用から30日間保持されるようになっています。

flutterawesome.com

kabochapo.hateblo.jp

この設定をカスタマイズすることで、更に通信量を減らして高速化することが可能です。
試しに最大10,000枚・1ヶ月間保持するように変更してみます。

やり方としては、まずdartファイルを作成して以下の設定を記述します。

import 'package:flutter_cache_manager/flutter_cache_manager.dart';

class CustomCacheManager extends CacheManager with ImageCacheManager {
  CustomCacheManager()
      : super(
          Config(
            'customCacheKey',
            stalePeriod: const Duration(days: 30), // NOTE: キャッシュ期間
            maxNrOfCacheObjects: 10000, // NOTE: キャッシュ上限枚数
          ),
        );
}

あとは、この設定を反映したい CachedNetworkImage ウィジェットcacheManager オプションで指定すればOKです。

CachedNetworkImage(
  imageUrl: "https://fakeimg.pl/300/?font_size=150",
  ...省略
  cacheManager: CustomCacheManager(),
),

これだけで、キャッシュ領域を拡大することができます!

3. サーバ側で画像を軽量化する

元々表示に使用していた画像が、デバイスで表示するには十分に解像度が高く、1枚1枚の容量が大きいという問題がありました。
これに対応するため、さくらインターネット社の ImageFlux を使用して軽量化したものを取得するように書き換えました。

imageflux.sakura.ad.jp

console.imageflux.jp

上記サイトの例で検証してみます。

かなり小さくなります!!
実際に表示に使用していた画像で試してみても、1枚あたり1/20以上の容量を削減できていました。

最後に

画像付きスクロールリストを高速化・通信量を減らす方法についてご紹介しましたが、いかがでしょうか。
Flutterを使うとシンプルなコードでとても簡単にリストを表示することができ、パッケージも豊富なためやりたいと思ったことが次々実現できるので、実装していて楽しいな〜と感じました😊