Flutter でスマートに非同期処理のエラーハンドリングをする方法

こんにちは。
本業では Android アプリのネイティブエンジニアをやっているものです。
以前は副業で Flutter を沢山書いていたのですが、最近は趣味で Flutter をよく書いています。

ところで、皆さんはエラーハンドリングに、悩んでませんか。

私はネイティブでも禿げるくらい悩みました（育毛の宣伝をしそうな雰囲気を醸し始めてしまっていますが、しません）
試行錯誤した末、 `iosched` の[このリポジトリ](https://github.com/google/iosched)のハンドリングが一番気に入っています。
※ `iosched` は超有名なお手本アプリです（ `NowInAndroid` の一つ前）


結論として、 Flutter （というか Dart ）においてもこのエラーハンドリングをしたいと考えました。
理由としては、この方法だとエラーハンドリングを強制しやすいからです。

元のソースコードを知らない人にとっては意味不明だと思うので、最初に `iosched` の非同期処理のエラーハンドリングの方法から解説します。
肝となっているのは `CoroutineUseCase` と `Result` です。


```kotlin: CoroutineUseCase
package com.google.samples.apps.iosched.shared.domain

import com.google.samples.apps.iosched.shared.result.Result
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import timber.log.Timber

/**
 * Executes business logic synchronously or asynchronously using Coroutines.
 */
abstract class UseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) {

    /** Executes the use case asynchronously and returns a [Result].
     *
     * @return a [Result].
     *
     * @param parameters the input parameters to run the use case with
     */
    suspend operator fun invoke(parameters: P): Result<R> {
        return try {
            // Moving all use case's executions to the injected dispatcher
            // In production code, this is usually the Default dispatcher (background thread)
            // In tests, this becomes a TestCoroutineDispatcher
            withContext(coroutineDispatcher) {
                execute(parameters).let {
                    Result.Success(it)
                }
            }
        } catch (e: Exception) {
            Timber.d(e)
            Result.Error(e)
        }
    }

    /**
     * Override this to set the code to be executed.
     */
    @Throws(RuntimeException::class)
    protected abstract suspend fun execute(parameters: P): R
}
```

```kotlin: Result
sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}
```

※ `Result` に関しては若干省略しています。


上記の二つでどうエラーハンドリングするかというと
1. `HogeUseCase` で abstract の `UseCase` を継承して `execute()` を override する
2. `HogeUseCase` を invoke して `Result` 型を返す

です。
abstract の `UseCase` を見てみると、非同期スレッド内で吐かれた例外を catch して `Result.Error` 型で返しています。
`execute()` に実装したコードを実行して try catch して `Result` 型でラップして返してくれるという動きをします。
ViewModel 側に常に UseCase をインジェクトしていればまずハンドリングが漏れないはずです。
Repository だと沢山メソッドがあってこうはできないので、やはり UseCase は必要だなと感じました。


ちょっと長くなってしまったのですが、 Dart においてもぜひこのハンドリングをしてみたいと思います！


# Result 型を作る
## 1. freezed を入れる

```yaml: pubspec.yaml
dependencies:
  build_runner: ^2.4.6
  flutter:
    sdk: flutter
  freezed: ^2.4.2
  freezed_annotation: ^2.4.1
```

コアとなるコードが外部のライブラリに依存してしまうのは不安ですが
1. Dart package であること（ Flutter への依存ではないだけかなりマシ）
2. いずれ Kotlin のように data class が導入されるであろうという希望的観測

を根拠に導入します。


## 2. freezed を使って Result 型を作る

```dart: result.dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'result.freezed.dart';

/// dart run build_runner watch --delete-conflicting-outputs
@freezed
abstract class Result<T> with _$Result<T> {
  const factory Result.success(T value) = Success<T>;

  const factory Result.error(Error error) = ResultError<T>;

  const factory Result.exception(Exception exception) = ResultException<T>;
}
```

part をつけ忘れないようにご注意ください。
あと、 `dart run build_runner watch --delete-conflicting-outputs` で自動生成も忘れずに。
ちなみに `error` と `exception` を分けたのは、 Dart が `Error` と `Exception` を区別しているからです。
また、`ResultError` や `ResultException` というちょっとダサい命名にした理由は、クラス名が `Exception` や `Error` とコンフリクトするからです。


早速 Result 型ができました。
使い方は後で説明します。

# UseCase を作る

```dart: usecase.dart
abstract class UseCase<I, O> {
  Future<Result<O>> call(I params) async {
    try {
      final data = await execute(params);
      return Success(data);
    } on Error catch (e) {
      return ResultError(e);
    } on Exception catch (e) {
      return ResultException(e);
    }
  }

  Future<O> execute(I params);
}

```

ほとんどそのまま同じように作れましたね！
`call()` は Kotlin でいうところの `invoke()` です。

UseCase と Result 型を定義して一番嬉しいのは、コンパイルエラーをフル活用して Error や Exception のハンドリングを強制できるところです
try catch を都度書く方針にしてたら私は絶対忘れます(※堂々と宣言しましたが自慢ではないです)

それでは次節で実際の使い方を見てみましょう。

# UseCase を使ってみる。
先ほど作った UseCase の使い方を見てみましょう。
試しに、「1を与えられた数で割った数値を返す。ただし、100超過の数は受け付けない」という UseCase を作ってみます。

```dart: get_divided_number_usecase.dart
class GetDividedNumberUseCase extends UseCase<int, double> {
  @override
  Future<double> execute(int params) async {
    await Future.delayed(const Duration(seconds: 1));

    if (params == 0) {
      throw Exception("Don't divide number by zero. ");
    }

    if (params > 100) {
      throw Error();
    }
    return 1 / params;
  }
}
```

こんな感じです。
割る数が 0 のときは Exception を投げて、 割る数が 100超過のときは Error を投げます。
使い方の確認も兼ねて簡単なテストを書きます。
確認したいことは下記の二点です。
1. `GetDividedNumberUseCase` を invoke したときにきちんと `Result` 型を返してくれるのか
2. 非同期スレッドから例外やエラーを投げたときに正しく catch できているか


わかりやすくするために ~~雑なテスト~~ シンプルなテストを書きました
```unit_test.dart
void main() {
  test("Failed by zero division", () async {
    final useCase = GetDividedNumberUseCase();
    final result = await useCase(0);
    result.when(
      success: (data) {
        assert(false);
      },
      error: (e) {
        assert(false);
      },
      exception: (e) {
        assert(true);
      },
    );
  });

  test("Divide one by two", () async {
    final useCase = GetDividedNumberUseCase();
    final result = await useCase(2);
    result.when(
      success: (data) {
        expect(0.5, data); // 丸め誤差はここでは無視
      },
      error: (e) {
        assert(false);
      },
      exception: (e) {
        assert(false);
      },
    );
  });

  test("Failed by dividing one by more than 100", () async {
    final useCase = GetDividedNumberUseCase();
    final result = await useCase(101);
    result.when(
      success: (data) {
        assert(false);
      },
      error: (e) {
        assert(true);
      },
      exception: (e) {
        assert(false);
      },
    );
  });
}
```


通りました。
`Result` 型の使い方がめちゃくちゃかっこよくないですか！？
こんなに Kotlin チックに書けるなんて・・・と深く感動しました。
今回の[サンプルリポジトリ](https://github.com/yoshimuratakuma0/dart_error_handling_sample)です。

皆さんもスマートなエラーハンドリング方法を思い付いたらぜひ共有してください！
ご覧いただきありがとうございました。