LoginSignup
1
0

More than 1 year has passed since last update.

【Flutter】ネットワークの画像をDioで取得して表示するWidget

Posted at

Image.networkの問題

URLを指定して画像を表示するWidgetとしてはImage.networkが使われます。通常はこのWidgetで事足りますが、HTTP通信にDioを使用したい、という用途には向きません。

Image.networkの実装ではdart:httpHttpClientで通信が実装されています。しかし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;
          }
        },
      );
    }
  }
}
  1. useStateで表示したい画像データ有無の状態を管理する
  2. useEffectで初回Build時のみ画像データのダウンロードを開始する
  3. 画像データをダウンロードする間はplaceHolderを表示する
  4. 画像データを取得したらImage.memoryで表示

実装の補足

  • 今回はhooksを使って実装しましたが、StatefulWidgetinitState(), setState()を利用しても実装できます
  • Image.memoryの引数frameBuilderを指定しないと、placeHolderに代わり取得した画像データを表示する一瞬の間だけなにも表示されないフレームが発生する場合があります。そこで、画像データの読み込みが完了するまでのフレームでは引き続きplaceHolderを表示させています。
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0