対象者
- Flutterで非同期処理を効率的に扱いたい開発者
- FutureとFutureOrの違いを深く理解したいプログラマー
- 非同期テストコードの実践的な例を探しているFlutterエンジニア
はじめに
Flutterの非同期プログラミングにおいて、Future
とFutureOr
は重要な役割を果たします。本記事では、これらの違いを具体的なテストコードを通じて解説します。理解を深めることで、コードの効率性と可読性を向上させ、より効果的なアプリ開発に役立てることができます。
非同期処理を理解する: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
の同期・非同期の両方の動作を検証しています。
-
最初の呼び出し
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回目の呼び出し
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回目の呼び出し
expect(await fetchData(), 13);
- 動作:
counter
が12なので、同期的に++counter
を返す。
- 動作:
-
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
を返すことができます。
まとめ
今回のコード解説を通じて、Future
とFutureOr
の使い分けや実践的な活用方法を学びました。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>>()));
});
});
}