通信をキャンセルしたい
UI操作に応じて通信など非同期処理をキャンセルしたい場面は多々あると思います。dartではhttp
ライブラリで通信まわりを実装する場合も多いですが、同じくらい人気のライブラリにdio
があります。そしてdio
にはなんとキャンセルのための仕組みが用意されています!早速使ってみましょう。
dio | Dart Package - Pub.dev
CancelToken class - dio library - Dart API - Pub.dev
テストしよう
具体的なUIとの連携などここで書くのは面倒なので、ユニットテストを書く体でdio
による通信とそのキャンセル操作を試してみます。
通信をモックする
dio
では実際に通信処理を行うクライアントObjectをプログラマーが指定できます。
HttpClientAdapter class - dio library - Dart API - Pub.dev
(例)デフォルト実装
final dio = Dio();
dio.httpClientAdapter = DefaultHttpClientAdapter();
単純にこのadapterを差し替えるだけで通信をモックできます。今回は通信をキャンセルするので、timeoutの間はレスポンスを返さないようなモックサーバを用意します。
/// 任意のリクエストに対してレスポンスを遅延させ、タイムアウトしたらErrorを投げるテスト用サーバ
class MockPendingServer implements HttpClientAdapter {
MockPendingServer({this.timeout = const Duration(seconds: 100)});
final Duration timeout;
@override
void close({bool force = false}) {
// nothing to do
}
@override
Future<ResponseBody> fetch(
RequestOptions options,
Stream<Uint8List>? requestStream,
Future? cancelFuture,
) async {
await Future<void>.delayed(timeout);
throw UnimplementedError(); // not reply valid response
}
}
CancelTokenのテスト
ではdio
でのキャンセルを試します。CancelToken
オブジェクトを一緒に渡して、キャンセルしたいタイミングでcancel()
を呼び出すだけ。通信がキャンセルされるとDioError
が投げられるので適当にcatchして確認します。
注意として、キャンセルされるまでawait dio.get
は完了しないので、Future.sync
によりtoken.cancel()
とは別の非同期処理中から呼び出しています。
void main() {
test("basic CancelToken usage", () async {
// 通信のモック
final dio = Dio()..httpClientAdapter = MockPendingServer();
final token = CancelToken();
final calling = Future.sync(() async {
try {
debugPrint("start");
await dio.get<dynamic>(
"http://test.com/hoge",
cancelToken: token, // CancelTokenを一緒に渡す
);
} on DioError catch (e) {
expect(e.type, DioErrorType.cancel);
debugPrint("cancelled");
return;
}
fail("DioError expected, but not received");
});
debugPrint("cancel");
token.cancel();
await calling;
});
}
実行結果からもキャンセルの様子が分かります。
start
cancel
cancelled
CancelableOperationでラップするテスト
CancelToken
をそのまま扱ってもいいですが、async
ライブラリに便利クラスがあるのでCancelableOperation
でラップしてみます。
CancelableOperation class - async library - Dart API - Pub.dev
注意 Future
をCancelableOperation
でラップするだけでは、CancelableOperation#cancel()
を呼び出しても元の非同期処理はキャンセルされません。後述の通り、onCancel
コールバックで適切なキャンセル処理を自前で定義する必要があります。
CancelableOperationの初期化
CancelableOperation
にはfromFuture
というfactoryコンストラクターしかありません。元の非同期処理をラップするように使用します。同時にonCancel
を渡すと、CancelableOperation#cancel()
のタイミングで呼び出してくれるので、onCancel
コールバックには元の非同期処理のキャンセル処理を置きます。今回で言えばCancelToken#cancel()
の呼び出しです。
final original = dio.get<dynamic>(
"http://test.com/hoge",
cancelToken: token,
);
final operation = CancelableOperation.fromFuture(
original,
onCancel: () => token.cancel(),
);
CancelableOperationの主なAPI
よく使うやつだけ紹介
-
cancel()
キャンセルする。factoryコンストラクタで渡したonCancel
を呼び出す。 -
value
ラップした元の非同期処理が完了したらthen
,エラーが発生したらcatchError
が呼ばれるようなFuture
を返すgetter。ただしcancel
が呼ばれた後では、ずっと終了しないFuture
を返す。 -
valueOrCancellation()
value
と似ているが、cancel
が呼ばれた後では指定した値(デフォルトはnull
)で終了するようなFuture
を返す。
テスト
先程と異なり、CancelableOperation
でラップすると、キャンセルしてもvalueOrCancellation()
のFuture
はDioError
を投げません。
void main() {
test("CancelableOperation", () async {
final dio = Dio()..httpClientAdapter = MockPendingServer();
final token = CancelToken();
final operation = CancelableOperation.fromFuture(
dio.get<dynamic>(
"http://test.com/hoge",
cancelToken: token,
),
onCancel: token.cancel,
);
final calling = Future.sync(() async {
debugPrint("start");
final res = await operation.valueOrCancellation(null);
debugPrint("cancelled");
expect(res, isNull);
});
debugPrint("cancel");
operation.cancel();
await calling;
});
}
出力結果は同じ。
start
cancel
cancelled