対象者
- FlutterのprecacheImageを知りたい人
- Flutterで画面起動と同時にすべての画像を表示させたい人
- Flutterで特定の処理が完了するまで、画面を表示させたくない人(上記では、特定の処理が「画像の読み込み」にあたる)
- Flutterで画像読み込み時に画像のサイズを知りたい人
はじめに
Flutterのアプリを起動させます。画面に画像を使ってますが、すこし残念なことに、起動直後は画像は出ず、しばらくしてから画像が表示されます。prechecheImageを使っても、起動後は良いのですが、起動から一瞬遅れて画像が表示されていました。
「画像読み込みまで最初の画面描画を遅らせる」という記事がありましたのが、そのままでは使えなかったので、使えるようにしたソースを解説します。
prechacheImageについて
StatefulWidgetのライフサイクルを使って、事前に画像を読み込んでおきます。
late Image image1;
@override
void initState() {
super.initState();
image1 = Image.asset("assets/logo.jpg");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
precacheImage(image1.image, context);
}
ただこの方法だと、「ボタンを押したら画像を表示する」というケースでは事前に読み込めます。しかし画面起動時には事前に読み込む時間がなく、最初の画面が表示されます。そのため、画面の描画があり、少し間を置いてから、画像の描画が始まります。(上記の動画の最初の方)。
ちなみに、precacheImageもimageCacheもなにかをimportする必要はなく、Flutterのソースにそのまま書けます。
キャッシュのクリア方法(未検証)
以下を使えばprecacheImageで読み込んだ画像を消せそう。(どうすれば検証できるかな。めっちゃ画像読み込むしかないかな)
imageCache.clear();
imageCache.clearLiveImages();
実施するソース
上記のサンプルコードの難点を解決するため、画像を読み込んでから画面の描画を開始するようにします。そうすることで、画面の描画と同時に画像を表示できるようになります。
void main() async {
final binding = WidgetsFlutterBinding.ensureInitialized();
binding.deferFirstFrame();
binding.addPostFrameCallback((_) {
final Element? context = binding.renderViewElement;
if (context != null) {
final image = const NetworkImage(imageUrl)
..resolve(const ImageConfiguration())
.addListener(ImageStreamListener((_, __) {
binding.allowFirstFrame();
}));
precacheImage(image, context);
}
});
}
runApp(const MyApp());
}
以下、徒然なるままに階説。
-
binding.deferFirstFrame()
フレームの描画をストップさせる -
binding.addPostFrameCallback
通常最初のフレームの描画が完了したら実行する関数を定義する。
描画が止められているから、そのまま実行しているのかな。 -
final Element? context = binding.renderViewElement;
いつもお世話になっているBuildContext、こんな風にも取得できるのか、、、 -
final image = const NetworkImage(imageUrl)
読み込む画像。AssetImageとかでも大丈夫です。 -
..resolve(const ImageConfiguration()).addListener(ImageStreamListener
画像を読み込み後に実行する関数を定義できる。
(余談) ImageStreamListenerの第一引数から、読み込んだ画像のサイズを確認することができる。 -
binding.allowFirstFrame();
フレームの描画を許可する。つまり、画像の読み込みが完了したら、フレームの描画を開始する。その結果、画像を読込終わった後で画面が描画される。画面と画像の表示のズレはなくなる、はず! -
precacheImage(image, context);
読み込んだ画像をキャッシュする。
まとめ
改めてリリースしたアプリでやってみると、画像と画面の描画のズレはアセットだと気になんないかなー、とか思ったりします。(おい!)
ただ、初回画面描画の画像表示のみならず、確実に実施したい処理が合ったときなど使えるかと思います。
参考
-
How do I clear Flutter’s image cache?
prechacheImageで読み込んだ画像を消去する方法
全ソース
「const precacheImageInMain = true;」 であれば、mainの中で画像を読み込んで、画面と画像を同時に描画します。false に変更すると、StatelessWidget内で画像読み込みするので、表示にすこし間ができます。
import 'package:flutter/material.dart';
const imageUrl = 'https://docs.flutter.dev/assets/images/dash/Dashatars.png';
const precacheImageInMain = true;
void main() async {
if (precacheImageInMain) {
final binding = WidgetsFlutterBinding.ensureInitialized();
binding.deferFirstFrame();
binding.addPostFrameCallback((_) {
final Element? context = binding.renderViewElement;
if (context != null) {
final image = const NetworkImage(imageUrl)
..resolve(const ImageConfiguration())
.addListener(ImageStreamListener((_, __) {
binding.allowFirstFrame();
}));
precacheImage(image, context);
}
});
}
runApp(const MyApp('precacheImageInMain: $precacheImageInMain'));
}
class MyApp extends StatelessWidget {
const MyApp(this.title, {Key? key}) : super(key: key);
final String title;
@override
Widget build(BuildContext context) {
if (!precacheImageInMain) {
precacheImage(const NetworkImage(imageUrl), context);
}
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: title),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
const Text('Hello Dash!'),
Image.network(imageUrl),
],
),
),
);
}
}