【Flutter】precacheImageで画像の初回表示を高速化する方法

に公開

はじめに

株式会社Sally 所属エンジニアの @wellPicker です。
弊社では、スマホやパソコンでマーダーミステリーを遊べるアプリであるウズや、マダミス情報・予約管理サイトマダミス.jp、マダミス開発ツールウズスタジオを開発しています。
そもそもマダミスとは何か? については、ぜひこちらもご確認ください。

ウズでは、さまざまな画面で多くの画像を表示する必要があります。
このようなモバイルアプリにおいて、「多数の画像をどうスムーズに見せるか」は重要な技術課題になります。
ネットワークを経由して画像を表示する場合、初回はその画像をリクエストしてダウンロードすることになるでしょう。これにより、実際に画像が表示されるまでに数秒程度のタイムラグが発生します。
通常であればこの程度のタイムラグはそこまで気になりませんが、例えばゲーム内の画像の表示など、タイムラグを極力許容したくない場面が存在します。
そこで今回は Flutter 標準 API の precacheImage() を用いて、上述したタイムラグをなるべく減らす方法をまとめます。

precacheImage() とは

precacheImage() は、その名の通り「指定された画像をイメージキャッシュにあらかじめフェッチする」ための関数です。
まずは、実際の実装を見てみましょう。

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  bool showImage = false;
  late final ImageProvider _provider;

  
  void initState() {
    super.initState();
    _provider = const NetworkImage('your_image_url');
  }

  
  void didChangeDependencies() {
    super.didChangeDependencies();
    precacheImage(_provider, context);
  }

  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('InstantImage Demo')),
        body: Center(
          child: showImage
              ? Image(image: _provider, fit: BoxFit.cover)
              : ElevatedButton(
                  onPressed: () {
                    setState(() {
                      showImage = true;
                    });
                  },
                  child: const Text('画像を表示'),
                ),
        ),
      ),
    );
  }
}

class InstantImage extends StatefulWidget {
  const InstantImage(this.url, {super.key});
  final String url;

  
  State<InstantImage> createState() => _InstantImageState();
}

class _InstantImageState extends State<InstantImage> {
  late final ImageProvider _provider;

  
  void initState() {
    super.initState();
    _provider = NetworkImage(widget.url);
  }

  
  Widget build(BuildContext context) {
    return Image(image: _provider, fit: BoxFit.cover);
  }
}

実際に画像を表示する時の時間が変わっている様子が確認できます。(ランダムな画像を表示するurlを使用したため、画像が異なる点はご了承ください)

キャッシュなし キャッシュあり

上記の実装では、あらかじめ MyApp の初期化時に precacheImage() を呼び出して、画像をイメージキャッシュに追加します。これにより、「画像を表示」ボタンを押したときにはすでに画像がキャッシュされているため、ユーザーの目には初めて触れる画像でもノータイムで表示できます。

注意事項としては、

  • precacheImage に渡したものと同じ ImageProvider を Image に渡す必要がある
  • あくまで「画像を表示するよりも前に読み込みをする」ための方法である。precacheImage() と Image を同時に呼び出しても precacheImage() が完了していないため、画像の表示を高速化できない

くらいでしょうか。

まだ、URL が動的に決まる場合(GraphQL や REST API を経由してURlを取得する場合)にも precacheImage() を使用することが可能です。
ただしその場合、「URL取得 → precacheImage() → 画像の表示」という順番で処理をすることになります。precacheImage() が完了するまではローディング画面を表示するフラグ管理などが必要になる点は注意しましょう。

キャッシュの成否を確認する方法

特に開発中の検証目的などで、実際のキャッシュの成否を確認したいこともあると思います。
Flutter では画像キャッシュは ImageCache クラスのインスタンスによって管理されています。呼び出す場合は、PaintingBinding.instance.imageCache を利用することでシングルトン化されたインスタンスを呼び出すことが可能です。
例えば PaintingBinding.instance.imageCache.ContainesKey() を呼び出すことで、その key に対応するキャッシュが残っているかを取得することができます。このとき、ContainesKey() の引数には ImageProvider を使用する必要があります。
またより詳細な情報が知りたい場合はPaintingBinding.instance.imageCache.StatusForKey() を使用することができます。StatusForKey() で得られる情報の詳細を理解するためには ImageCache そのものの仕組みを理解する必要がありますが、今回は割愛します。内部で Image に対する listener を張って〜、みたいなことをしているらしいですが詳細は不明です。

キャッシュ失敗のよくある原因

ImageCache の上限を超えてしまった

ImageCache には、保存できるキャッシュの件数、およびバイトサイズでの上限が定められています。これらの値は PaintingBinding.instance.imageCache.maximumSize および PaintingBinding.instance.imageCache.maximumSizeBytes を使用して変更することが可能です。
また、前述した ContainesKey() や StatusForKey() を使うことで、対応するキャッシュが生存しているかどうかを調べることができます。printデバッグなどで、対応するキャッシュが途中で消されていると判明したらこれが原因でしょう。

precacheImage() の際に失敗した

precacheImage() 時に、一度に大量のリクエストをサーバーに送ったせいで 503 エラーが発生したりする場合があります。この場合、事前読み込みに失敗しているため Image の描画時にはキャッシュがないものとして処理が行われます。

ImageProvider の設定が間違っている

前述した通り、precacheImage() と実際の Image で使用する ImageProvider は同じものである必要があります。precacheImage() だけ呼び出して、同じurlで Image を実装しても、ImageProvider の設定が間違っているとキャッシュが効かないことがあるようです。

まとめ

今回は、precacheImage() を使用して画像を事前読み込みし、画像表示を高速化する方法を説明しました。
また、関連する ImageCache 周りの基本的な解説もしました。
モバイルアプリにおいて、画像はUI/UXに大きく関わってくる要素であり、precacheImage() のようなテクニックを積み重ねていくことでユーザーの体験も大きく左右されます。この記事が皆様のアプリの体験向上に繋がれば幸いです。

UZU テックブログ

Discussion