Image.network
の問題
URLを指定して画像を表示するWidgetとしてはImage.network
が使われます。通常はこのWidgetで事足りますが、HTTP通信にDio
を使用したい、という用途には向きません。
Image.network
の実装ではdart:http
のHttpClient
で通信が実装されています。しかしhttp
ライブラリはやや不便な点があり、特にinterceptorを利用した柔軟な処理はdio
に軍配が上がると思います。
デバッグモードでの差し替えの検討
Image.network
の実装が使用するHttpClient
インスタンスをデバックモードでのみ差し替える機会が用意されています。次のようなグローバルな変数を書き換えればよさそうです。
debug.dart
/// Provider from which [NetworkImage] will get its [HttpClient] in debug builds.
///
/// If this value is unset, [NetworkImage] will use its own internally-managed
/// [HttpClient].
///
/// This setting can be overridden for testing to ensure that each test receives
/// a mock client that hasn't been affected by other tests.
///
/// This value is ignored in non-debug builds.
HttpClientProvider? debugNetworkImageHttpClientProvider;
ただ、Dio
クラスを使うには別途アダプターが必要・デバッグモード限定、と制約が多いのでやめました。
Widgetを自作する
作った方が早い。
指定されたURLの画像をDio
で取得してImage
に表示します。読み込み中は代わりのWidgetを表示することにしましょう。画像のデータUnit8Lit
を状態に持つWidgetですが、今回はflutter_hooks
を利用します。
pubspec.yaml
dependencies:
+ dio: ^4.0.6
+ flutter_hooks: ^0.18.3
my_network_image.dart
class MyNetworkImage extends HookWidget {
const MyNetworkImage({
Key? key,
required this.dio,
required this.srcPath,
required this.placeHolder,
this.fit,
}) : super(key: key);
final Dio dio;
final String srcPath;
final Widget placeHolder;
final BoxFit? fit;
@override
Widget build(BuildContext context) {
// 画像データ
final imageData = useState<Uint8List?>(null);
useEffect(
// 初回のbuild時のみ実行
() {
// 画像データの取得
final cancelToken = CancelToken();
final call = dio.get<List<int>>(
srcPath,
cancelToken: cancelToken,
options: Options(
responseType: ResponseType.bytes,
followRedirects: false,
),
);
call.then((response) {
final data = response.data;
if (data == null) throw Exception("empty data");
final bytes = Uint8List.fromList(data);
imageData.value = bytes;
}).onError((e, _) {
// エラーハンドリング(キャンセル含む)
if (e is DioError && e.type == DioErrorType.cancel) {
debugPrint("loading image data cancelled: $srcPath");
} else {
debugPrint("fail to load image data: $srcPath\n caused by $e");
}
});
return () {
// 画像データの取得が終わる前にWidgetがdisposeされたらキャンセルする
cancelToken.cancel("widget disposed");
};
},
[],
);
final bytes = imageData.value;
if (bytes == null) {
return placeHolder;
} else {
return Image.memory(
bytes,
fit: fit,
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded || frame != null) {
// すでに取得済みの場合
return child;
} else {
// render前
return placeHolder;
}
},
);
}
}
}
-
useState
で表示したい画像データ有無の状態を管理する -
useEffect
で初回Build時のみ画像データのダウンロードを開始する - 画像データをダウンロードする間は
placeHolder
を表示する - 画像データを取得したら
Image.memory
で表示
実装の補足
- 今回はhooksを使って実装しましたが、
StatefulWidget
でinitState(), setState()
を利用しても実装できます -
Image.memory
の引数frameBuilder
を指定しないと、placeHolder
に代わり取得した画像データを表示する一瞬の間だけなにも表示されないフレームが発生する場合があります。そこで、画像データの読み込みが完了するまでのフレームでは引き続きplaceHolder
を表示させています。