【Dart】FutureOrを用いた非同期実装

  • 2024年12月5日
  • 2024年12月5日
  • Dart

対象者

  • Flutterで非同期処理を効率的に扱いたい開発者
  • FutureとFutureOrの違いを深く理解したいプログラマー
  • 非同期テストコードの実践的な例を探しているFlutterエンジニア

はじめに

Flutterの非同期プログラミングにおいて、FutureFutureOrは重要な役割を果たします。本記事では、これらの違いを具体的なテストコードを通じて解説します。理解を深めることで、コードの効率性と可読性を向上させ、より効果的なアプリ開発に役立てることができます。

非同期処理を理解する:FutureとFutureOrの実践例

テスト

まず、テストコード全体を見てみましょう。

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';

void main() {
  Future<int> loadData() async {
    await Future.delayed(const Duration(milliseconds: 10));
    return 11;
  }

  var counter = 0;

  FutureOr<int> fetchData() async {
    if (0 < counter) {
      return ++counter;
    }

    return loadData();
  }

  test('Future', () async {
    expect(await loadData(), 11);
  });

  test('FutureOr', () async {
    Stopwatch stopwatch = Stopwatch();
    stopwatch.start();

    final FutureOr<int> result1 = fetchData();
    expect(await result1, 11);
    counter = await result1;

    final elapsedTime1 = stopwatch.elapsedMilliseconds;
    expect(elapsedTime1, greaterThanOrEqualTo(10));

    final FutureOr<int> result2 = fetchData();
    expect(await result2, 12);

    final elapsedTime2 = stopwatch.elapsedMilliseconds;
    expect(elapsedTime2, lessThan(15));

    expect(await fetchData(), 13);

    counter = 0;
    final FutureOr<int> result3 = fetchData();
    expect(await result3, 11);
    final elapsedTime3 = stopwatch.elapsedMilliseconds;
    expect(elapsedTime3, greaterThanOrEqualTo(20));

    stopwatch.stop();
  });
}

loadData関数の解説

loadDataは常に非同期的に整数を返す関数です。

Future<int> loadData() async {
  await Future.delayed(const Duration(milliseconds: 10));
  return 11;
}
  • 動作:10ミリ秒の遅延後に11を返します。
  • ポイントFuture<int>を返すため、常に非同期処理となります。

fetchData関数の解説

fetchDataは条件に応じて同期または非同期で整数を返します。

FutureOr<int> fetchData() {
  if (0 < counter) {
    return ++counter;
  }

  return loadData();
}
  • 動作
    • counterが0より大きい場合:同期的に++counterを返す。
    • counterが0の場合:loadData()を呼び出し、非同期的に11を返す。
  • ポイントFutureOr<int>を返すため、戻り値が同期・非同期のいずれかになります。通常のFutureであれば、Future.value(++counter)と書くところです。しかし、FutureOrのおかげで++counterと書けます。もちろん、Future.value(++counter)でもコンパイルエラーにはなりません。

実際のコードでは、最初にキャッシュがないのでデータを非同期処理で取得して、2回目以降はキャッシュデータに基づいた値を返すことを想定してます。キャッシュはあまりデータ元で変更されない前提です。

テストケースの詳細

test('Future')の解説

test('Future', () async {
  expect(await loadData(), 11);
});
  • 目的loadDataが正しく11を返すかを確認。
  • ポイントawaitを使用して非同期結果を取得。

test('FutureOr')の解説

このテストでは、fetchDataの同期・非同期の両方の動作を検証しています。

  1. 最初の呼び出し

     final result1 = fetchData();
     expect(result1, isA<Future<int>>());
     expect(await result1, 11);
     counter = await result1;
    
     final elapsedTime1 = stopwatch.elapsedMilliseconds;
     expect(elapsedTime1, greaterThanOrEqualTo(10));
    
    • 動作counterが0なので、loadData()を非同期的に実行。戻り値の型は、Future<int>
    • 計測:累積時間が10ミリ秒掛かっていることを確認できるので、loadData()が実行されたことが分かる
  2. 2回目の呼び出し

     final result2 = fetchData();
     expect(result2, isA<int>());
     expect(result2, 12);
    
     final elapsedTime2 = stopwatch.elapsedMilliseconds;
     expect(elapsedTime2, lessThan(15));
    
    • 動作counterが11なので、同期的に++counterを返す。戻り値の型は、int
    • 計測:累積時間が15ミリ秒掛かっていないことを確認できるので、loadData()が最初の1回目しか実行されてないことが分かる
  3. 3回目の呼び出し

    expect(await fetchData(), 13);
    
    • 動作counterが12なので、同期的に++counterを返す。
  4. counterのリセットと再検証

    counter = 0;
    final FutureOr<int> result3 = fetchData();
    expect(await result3, 11);
    final elapsedTime3 = stopwatch.elapsedMilliseconds;
    expect(elapsedTime3, greaterThanOrEqualTo(20));
    
    • 動作counterを0にリセットし、再び非同期処理を実行。
    • 計測:累積時間が20ミリ秒以上であることを確認。(非同期処理を合計2回実行したため)

子クラスでのメソッドの実装

親クラスで戻り値FutureOr<String>で定義されたメソッドは、子クラスでFutureOr<String>Future<String>Stringのいずれでもメソッドを定義できる。

abstract class Parent {
  FutureOr<String> loadData();
}

class Child1 implements Parent {
  @override
  Future<String> loadData() => Future.value('result');
}

class Child2 implements Parent {
  @override
  String loadData() => 'result';
}

class Child3 implements Parent {
  @override
  FutureOr<String> loadData() => 'result';
}

test('継承', () {
  final result1 = Child1().loadData();
  expect(result1, isA<Future<String>>());

  final result2 = Child2().loadData();
  expect(result2, isA<String>());

  final result3 = Child3().loadData();
  expect(result3, isA<FutureOr<String>>());
  expect(result3, isA<String>());

  expect(result3, isNot(isA<Future<String>>()));
});

Q&A

Q1. なぜfetchDataの戻り値にFutureOr<int>を使うのですか?

A1. fetchDataは条件により同期的にも非同期的にも整数を返すため、FutureOr<int>を使用しています。これにより、戻り値がintまたはFuture<int>のどちらかになることを示せます。

Q2. awaitを使用せずにfetchDataの結果を取得できますか?

A2. 可能ですが、戻り値がFutureの場合もあるため、awaitを使用して結果を安全に取得することが推奨されます。awaitを使用しないと、意図せずFutureオブジェクトが返される可能性があります。

Q3. loadData関数内でawaitを使用する理由は何ですか?

A3. Future.delayedによる遅延を待つためにawaitを使用しています。これにより、指定した遅延時間後に処理が進行し、11を返すことができます。

まとめ

今回のコード解説を通じて、FutureFutureOrの使い分けや実践的な活用方法を学びました。Futureは常に非同期処理を表すのに対し、FutureOrは同期・非同期の両方を扱える柔軟性があります。これらを適切に使いこなすことで、Flutterアプリの効率性とユーザー体験を向上させることができます。

参考

ソース(main.dartにコピペして動作確認用)

import 'dart:async';

import 'package:flutter_test/flutter_test.dart';

abstract class Parent {
  FutureOr<String> loadData();
}

class Child1 implements Parent {
  @override
  Future<String> loadData() => Future.value('result');
}

class Child2 implements Parent {
  @override
  String loadData() => 'result';
}

class Child3 implements Parent {
  @override
  FutureOr<String> loadData() => 'result';
}

void main() {
  group('基本', () {
    Future<int> loadData() async {
      await Future.delayed(const Duration(milliseconds: 10));
      return 11;
    }

    var counter = 0;

    FutureOr<int> fetchData() {
      if (0 < counter) {
        return ++counter;
      }

      return loadData();
    }

    test('Future', () async {
      expect(await loadData(), 11);
    });

    test('FutureOr', () async {
      Stopwatch stopwatch = Stopwatch();
      stopwatch.start();

      final result1 = fetchData();
      expect(result1, isA<Future<int>>());
      expect(await result1, 11);
      counter = await result1;

      final elapsedTime1 = stopwatch.elapsedMilliseconds;
      expect(elapsedTime1, greaterThanOrEqualTo(10));

      final result2 = fetchData();
      expect(result2, isA<int>());
      expect(result2, 12);

      final elapsedTime2 = stopwatch.elapsedMilliseconds;
      expect(elapsedTime2, lessThan(15));

      expect(await fetchData(), 13);

      counter = 0;
      final FutureOr<int> result3 = fetchData();
      expect(await result3, 11);
      final elapsedTime3 = stopwatch.elapsedMilliseconds;
      expect(elapsedTime3, greaterThanOrEqualTo(20));

      stopwatch.stop();
    });
  });
  group('継承', () {
    test('継承', () {
      final result1 = Child1().loadData();
      expect(result1, isA<Future<String>>());

      final result2 = Child2().loadData();
      expect(result2, isA<String>());

      final result3 = Child3().loadData();
      expect(result3, isA<FutureOr<String>>());
      expect(result3, isA<String>());

      expect(result3, isNot(isA<Future<String>>()));
    });
  });
}