【Flutter】ネットワーク画像の読み込み直後にイベントを実行

対象者

  • Flutterアプリで、ネットワークから画像をダウンロードが完了し、表示されるときの処理を設定したい

はじめに

チャットアプリの外観を実装しています。
メッセージを入力したときに画面がスクロールダウンして一番下に行くようにします。しかし画像があると、メッセージ入力直後にスクロールダウンしても、画像は読み込まれていないため、画像がサイズ0として計算された状態でスクロールされました。そして、その後、読み込まれた画像は画面外に表示されます。
そこで、ネットワークの画像をダウンロードが完了して表示されたときにスクロールダウンするようにするにはどうすれば良いのかと、調査しました。Image.network の引数 frameBuilder を設定すれば良いと分かりました。ただ、こちらは読込中に何度も呼ばれます。また、読込されて表示された画像が一度画面外にいき、スクロールして再度画面内に入り表示されたときも実行されることが分かりました。そこで引数を調査し、ネットワークからの読込直後にのみ実行されるような設定を見つけました。
調査中に、画像をネットワークから読み込んでいる途中のWidgetも設定できるようなので、記載しておきます。

実施するソース

Image.network(
  url,
  loadingBuilder: (
     BuildContext context,
     Widget child,
     ImageChunkEvent? loadingProgress) {
      if (loadingProgress != null) {
       return const CircularProgressIndicator();
      }
      return child;
   },
  frameBuilder: (
     BuildContext context,
     Widget child,
     int? frame,
     bool wasSynchronouslyLoaded) {
      if (!wasSynchronouslyLoaded && frame == 0) {
        print('image was loaded');
      }
      print('frameBuilder was called: ${_counter++}');
      return child;
  },
)

loadingBuilder に読込中のWidgetの定義、frameBuilder に読込後のWidgetを定義します。
childには本来のWidget が入っているようなので、なにもなければchildをそのまま返します。

  • loadingBuilder
    読込中のWidgetの定義
    childには本来のWidget が入っているようなので、なにもなければchildをそのまま返します。
    loadingProgressには、画像のバイト数と、読込済みのバイト数が取得できる。こちらがnullでない場合、読込中なのでCircularProgressIndicatorを表示するように設定している

  • frameBuilder
    読込後のWidgetを定義
    childには本来のWidget が入っているようなので、なにもなければchildをそのまま返します
    「!wasSynchronouslyLoaded && frame == 0」が何を意味しているかは分からないが(値を表示して調整した)、この組み合わせなら読込直後のみ実行されるようになった。

まとめ

Image.network で画像読み込む後の処理ができるようにしました。無事画像読み込む後にスクロールダウンする処理も実装できました。
Image.networkなんて、Flutterを始めたときから使ってますが、知らない機能が埋まっているもんですね。

参考

ChatGPTがframeBuilderを教えてくれたので、それを元に色々試した。
ChatGPTにはaddListnerを使用して読込直後の判別を進められたが、引数の組み合わせで解決できた。

全ソース

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

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> {
  List<String> imageUrls = [
    'https://flutter.salon/wp-content/uploads/2022/11/IMGP0818-768x508.jpg',
    'https://flutter.salon/wp-content/uploads/2023/03/IMGP5710-768x514.jpg',
    'https://flutter.salon/wp-content/uploads/2023/03/IMGP5591-768x514.jpg',
    'https://flutter.salon/wp-content/uploads/2023/03/IMGP4555-768x514.jpg',
    'https://flutter.salon/wp-content/uploads/2023/02/IMGP8968-768x514.jpg',
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
          itemCount: imageUrls.length,
          itemBuilder: (context, index) =>
              NetworkImageWithEvent(imageUrls[index])),
    );
  }
}

class NetworkImageWithEvent extends StatelessWidget {
  static int _counter = 0;
  const NetworkImageWithEvent(
    this.url, {
    super.key,
  });

  final String url;

  @override
  Widget build(BuildContext context) {
    return Image.network(
      url,
      loadingBuilder: (
        BuildContext context,
        Widget child,
        ImageChunkEvent? loadingProgress,
      ) {
        if (loadingProgress != null) {
          return const CircularProgressIndicator();
        }
        return child;
      },
      frameBuilder: (
        BuildContext context,
        Widget child,
        int? frame,
        bool wasSynchronouslyLoaded,
      ) {
        if (!wasSynchronouslyLoaded && frame == 0) {
          print('image was loaded');
        }
        print('frameBuilder was called: ${_counter++}');
        return child;
      },
    );
  }
}