LoginSignup
1
2

Dart の Future.wait() における型の扱いが危なかしい問題

Last updated at Posted at 2023-11-21

こんにちは。
本業では Android アプリエンジニア (Native) をしているものです。
Flutter は楽しいですね。
最近は趣味で Flutter を書いています。以前は副業で過労○しそうなくらい書いてました。
KMM や Compose Multiplatform などいくつかのマルチプラットフォーム対応のフレームワークを試しましたが、今のところ Flutter が一番アプリを作りやすいです。
Compose Multiplatform に関しては、せめて Navigation だけでも誰か作って欲しいです(他力本願)

本題なのですが、 皆さんは Dart の Future.wait() を使ったことはありますでしょうか。
複数の Future を並列で実行して待つというものです。
具体的な用途としては、例えば複数の API を同時に叩くときにシーケンシャルに叩く場合に比べて効率良くレスポンスを受け取れます。
私も何度か使ったのですが、使うたびに「型指定が実質不可能で怖いな」という感想を抱きます。

問題点

一応 wait() にはジェネリクスを指定できるようになってはいます。下記が wait() メソッドの定義です。

future.dart
  static Future<List<T>> wait<T>(Iterable<Future<T>> futures,
      {bool eagerError = false, void cleanUp(T successValue)?}) {

ただし、並列実行したときの返り値が全て同じ型に準拠をしている前提なので、このジェネリクスを活用できることは稀だと思います。結果、dynamic な型を受け取ることになり、ダウンキャストする危険が生じます。
個人開発中のアプリの実例ですが↓

    final futures = await Future.wait([
      canvasRepository.fetchTextEntities(projectId: params),
      canvasRepository.fetchArrowEntities(projectId: params),
    ]);
    final texts = futures[0] as Iterable<TextEntity>;
    final arrows = futures[1] as Iterable<ArrowEntity>;

危ないところが盛り沢山だと思いませんか?(思わなかったらすみません)

コンパイルエラーを全く活用できていないコードとなってしまい、ランタイムエラーで落ちるという最悪のケースが発生しかねません。

私が問題に思ったのは下記の二点です。

  1. 返り値の配列にインデックスアクセスするのが危険(例えば、配列の中身を一個消したがインデックスを参照する部分を消し忘れてしまい、 OutOfIndex の Exception を吐く)
  2. 返り値の型が変わってもダウンキャストしているのでコンパイルエラーを吐かない

「コンパイルさえ通ればうまく動いて欲しい」という怠惰を拠り所に解決策を考えました。

解決策

wait() はうまく使えなかったので、 Util を作ってしまいました。
「アプリ固有のロジックを持っていないため、セーフ」と自分に強く言い聞かせています。
簡単に言うと、 Future.wait() をラップして型の扱いを厳格にしています。

future_utils.dart
class FutureUtils {
  FutureUtils._();

  static Future<Pair<S, T>> waitPair<S, T>(
    Pair<Future<S>, Future<T>> pair,
  ) async {
    final futures = await Future.wait([pair.first, pair.second]);
    return Pair(futures[0] as S, futures[1] as T);
  }
}
pair.dart
class Pair<S, T> {
  final S first;
  final T second;

  Pair(
    this.first,
    this.second,
  );
}

先程の危なかしかったコードを実際に書き換えてみます。

Before

    final futures = await Future.wait([
      canvasRepository.fetchTextEntities(projectId: params),
      canvasRepository.fetchArrowEntities(projectId: params),
    ]);
    final texts = futures[0] as Iterable<TextEntity>;
    final arrows = futures[1] as Iterable<ArrowEntity>;

After

    final futures =
        await FutureUtils.waitPair<Iterable<TextEntity>, Iterable<ArrowEntity>>(
      Pair(
        canvasRepository.fetchTextEntities(projectId: params),
        canvasRepository.fetchArrowEntities(projectId: params),
      ),
    );
    final texts = futures.first;
    final arrows = futures.second;

配列アクセスもダウンキャストもなくなりました。
fetch の返り値の型が変わったらしっかりコンパイルエラーが教えてくれます。
うっかり消したはずの配列にアクセスしてしまったりもしません。
デグレしにくいコードになりました。

ここには Pair のみを載せましたが、 Triple + waitTriple() なんかも同じ要領で作れます。

Util を作るというあまりスマートではない解決方法ですが、アプリ固有のロジックが含まれていないため十分に汎用性が高く、解決したい課題に対してピンポイントに効いたのでめでたしめでたしです。

それでは!

1
2
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
2