dart/flutterでキャンセル可能な非同期処理を実装する

公式ドキュメントにサンプルがない?ので結構ハマった。ググって出てきたサンプルもなんか上手くいかなかった。試行錯誤して多分これで大丈夫そうなのでメモしておく。

CancelableOperationを使う

キャンセル不要の非同期処理の実装はCompletionを使うことが多いので、CancelableCompletionを使うべきかと思ったけど、どう使っていいかわからなかった。あれこれ試してCancelableOperationですっきり実装できたので、こちらを使う。ちなみにCancelableOperation#fromFutureを使うことになるが、ドキュメントにこれを使うとCancelableCompletionを使うのと同じようなことだと書いてあった。

CancelableOperation.fromFuture constructor - CancelableOperation - async library - Dart API

Calling this constructor is equivalent to creating a CancelableCompleter and completing it with result.

CancelableOperation#fromFuture

CancelableOperationを生成する際は、CancelableOperation#fromFutureで生成するのが基本。

  • 第一引数に非同期処理を行うFutureを渡す。以後、メイン処理と呼ぶ。
  • 第二引数のonCancelでキャンセル処理を待つFutureを渡す。以後、キャンセル待ち処理と呼ぶ。

メイン処理

とりあえずわかりやすいサンプルを書く。

  • 1秒待つループを10回繰り返して約10秒待つ処理をする
    • 本来は時間のかかる処理をする。通信とかファイルロードとか
  • 戻り値は正常終了したらtrueを返すようにする
    • 本来は非同期処理で返したい結果を返す。結果が必要なければvoidでいい
    • キャンセルした時はnullを返す。別に値を返しても良いが、いずれにせよキャンセルした時の結果は仕組み上参照されないだろう
bool _isCancelRequested = false;

Future<bool?> _mainFuture() {
   for (int i = 0; i < 10; i++) {
     await Future.delayed(Duration(seconds: 1));
     if (_isCancelRequested) return null;
   }

   return true;
}

最後まで終了する前に、_isCancelRequested = trueになれば、途中で処理が終わることになる。

キャンセル待ち処理

キャンセル処理で覚えておくこと

  • 非同期処理外からはCancelableOperation#cancelをコールしてキャンセルする
  • キャンセル処理終了まで時間がかかることもある
  • CancelableOperation#cancelはawaitするとキャンセル終了を待つことができる
  • ただしそのためには自分でキャンセル終了待ちの非同期処理を実装する必要がある
  • キャンセル終了までの非同期処理はCancelableOperation#fromFutureの引数onCancelに渡す
CancelableOperation? _operation;

Future<void> _waitCancelCompletion() {
    // 並行して走っているmainFutureの終了を待つ
    return Future.doWhile(() async {
      // 待ち時間は適当に・・・
      await Future.delayed(const Duration(milliseconds: 300));
      return !(_operation.isCanceled || _operation.isCompleted);
    });
}

上記をラッピングした非同期処理クラスを作る

実際にビルドして動かしてはないです。が、やるべきことはわかるはず。

import 'dart:async';

import 'package:async/async.dart';
import 'package:flutter/foundation.dart';

/// 非同期処理の結果。キャンセルしたかどうかもわかるようにしている
class AsyncCancelableResult {
  final bool isCanceled;
  final bool? value;

  const AsyncCancelableResult({required this.isCanceled, required this.value});
}

/// CancelableOperationのラッパークラス
class SampleCancelableOperation {
  /// ここの<bool>は非同期処理が正常終了した時、返したい型にする
  late CancelableOperation<bool> _operation;
  bool _isCancelRequested = false;

  Future<AsyncCancelableResult> start() {
    final completer = Completer<AsyncCancelableResult>();
    final operation = CancelableOperation<bool>.fromFuture(
        _mainFuture(),
        onCancel: () => _waitCancelCompletion()
    );
    // thenで処理開始前にセットしておく
    _operation = operation;

    operation.then((value) {
      // mainProcessが正常終了した時、ここに来る
      // valueはmainProcessの戻り値。つまりtrue
      completer.complete(AsyncCancelableResult(isCanceled: false, value: value));
    }, onError: (e, s) {
      // mainProcessで例外をthrowした時、ここに来る
      completer.completeError(e);
    }, onCancel: () {
      // cancelをコールし、_waitCancelCompletionが終了した時、ここに来る
      completer.complete(AsyncCancelableResult(isCanceled: true, value: null));
    });

    return completer.future;
  }

  Future<bool?> _mainFuture() {
    for (int i = 0; i < 10; i++) {
      await Future.delayed(Duration(seconds: 1));
      if (_isCancelRequested) return null;
    }

     return true;
  }

  /// キャンセルしたい時に呼ぶ
  /// awaitで呼ぶとキャンセル終了するまで待つ
  Future<void> cancel() async {
    if (_isCancelRequested || _operation.isCanceled || _operation.isCompleted) return;

    _isCancelRequested = true;
    // このawaitは_waitCancelCompletion()の終了を待つ
    await _operation.cancel();
  }

  Future<void> _waitCancelCompletion() {
    return Future.doWhile(() async {
      await Future.delayed(const Duration(milliseconds: 300));
      return !(_operation.isCanceled || _operation.isCompleted);
    });
  }
}

SampleCancelableOperationを以下のようにして使うことができる。

SampleCancelableOperation? _operation;

// どこかの関数処理
try {
 _operation = SampleCancelableOperation();
  final result = await operation.start();
  if (result.isCanceled) {
    // キャンセルした時の処理
  } else {
    // 正常した時の処理
    // trueが出力されるはず
    print(result.value!);
  }
} catch (e) {
  // _mainFutureから例外をthrowするとここに来る
}

// 上とは別のどこかの関数処理
// キャンセルし、その終了まで待つ
await _operation?.cancel();