【Flutter】dio + freezed でAPIレスポンスをResult<T>で受け取る

# 想定読者

* FlutterでのAPIレスポンスのハンドリングに`Result<T>` を使いたい
* swiftの `Union` や kotlinの `sealed class`のようなものをFlutterで扱いたい

# はじめに

普段Androidを書いているときに [`retrofit`](https://square.github.io/retrofit/) から受け取ったAPIレスポンスを [`sealed class`](https://kotlinlang.org/docs/reference/sealed-classes.html) を利用し、以下のように変換しています。

```kotlin
sealed class Response<T> {
  data class Success<T>(val body: T?) : Response<T>()
  data class Error<T>(val type ErrorType) : Response<T>()

  companion object {
    fun create<T>(retrofitResponse: retrofit2.Response<T>): Response<T> {
      return when {
        response.isSuccessful -> Success(response.body())
        else -> Error(ErrorType.create(response))
      }
    }
  }
}
```

`Response` に変換することでViewModel層などでは、`retrofit` に依存せず、`when文` を利用し、レスポンスをさばくことが可能になります。

```kotlin
fun request(keyword: String) {
  viewModelScope.launch {
    when (val res = repository.fetchSomething(keyword)) {
      is Success -> res.body?.let { _elements.value = it }
      is Error -> _errorType.value = res.type
    }
  }
}
```


最近Flutterを書いていて、上記と同じようなことをしたいと考え、以下のように実装してみました。

至らない点もあると思うので、ぜひコメント等でご指摘いただけたら嬉しいです。


# dio

HTTP Clientライブラリです。

Flutterの公式では `http` というライブラリを推していますが
個人的にはAndroidで [`OkHttp`](https://square.github.io/okhttp/) を利用していたこともあり、`dio` の書き方がそれに似ている感触を得たので、`dio`を利用しています。

また、Githubのstar数が異常に多く、日本語の記事は少ないものの、英語での記事は大量にあったため安心して選定しました。

公式のリファレンスにもありますが、`Dio`クラスは`abstract class`なので、以下のようにアプリ独自のクラスを作ります。

```dart
class MyDio with DioMixin implements Dio{
  // ...
}
```
（公式 [Extends](https://pub.dev/packages/dio#extends-dio-class) より引用）

簡単なリクエストは以下のようにかけます

```dart
Response response;
Dio dio = new Dio();
response = await dio.get("/test?id=12&name=wendu");
print(response.data.toString());
// Optionally the request above could also be done as
response = await dio.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());
```

（公式 [Examples](https://pub.dev/packages/dio#examples) より引用）

# freezed

Dartでは`sealed class`のようなものがありません。
ただ、同様のものを生成してくれるライブラリとして [`freezed`](https://pub.dev/packages/freezed)があります。

詳細は割愛します。（公式を読んでいただけたらわかると思うので。）

```dart
@freezed
abstract class Union<T> with _$Union<T> {
  const factory Union(T value) = Data<T>;
  const factory Union.loading() = Loading<T>;
  const factory Union.error([String message]) = ErrorDetails<T>;
}
```

（公式 [The syntax](https://pub.dev/packages/freezed#the-syntax) より引用）


# 実装

`Result`クラスは以下のように設定しております。
（簡略化のため、importなどは省略しております。）

`result.dart`

```dart
@freezed
abstract class Result<T> with _$Result<T> {
  const factory Result.success(T value) = Success<T>;
  const factory Result.failure(Error error) = Failure<T>;
}
```

以下の記事を参考に作成しました。
[Handling Network Calls and Exceptions in Flutter](https://dev.to/ashishrawat2911/handling-network-calls-and-exceptions-in-flutter-54me)

余談ですが、`Error`クラスは以下のようにつくっています。

```dart
@freezed
abstract class Error with _$Error {
  const factory Error.requestCancelled() = _RequestCancelled;

  const factory Error.unauthorisedRequest() = _UnauthorisedRequest;

  const factory Error.requestError({ ApiError apiError }) = _RequestError;

  const factory Error.serviceUnavailable() = _ServiceUnavailable;

  const factory Error.sendTimeout() = _SendTimeout;

  const factory Error.noInternetConnection() = _NoInternetConnection;

  const factory Error.unexpectedError() = _UnexpectedError;

  const Error._();

  static Error getApiError(error) {
    if (error is Exception) {
      try {
        Error _error;
        if (error is DioError) {
          switch (error.type) {
            case DioErrorType.CONNECT_TIMEOUT:
            case DioErrorType.SEND_TIMEOUT:
            case DioErrorType.RECEIVE_TIMEOUT:
              _error = Error.sendTimeout();
              break;
            case DioErrorType.CANCEL:
              _error = Error.requestCancelled();
              break;
            case DioErrorType.RESPONSE:
              final statusCode = error.response.statusCode;
              if (400 <= statusCode && statusCode < 500) {
                _error = Error.requestError(apiError: ApiError.fromJson(error.response.data));
              } else if (500 <= statusCode) {
                _error = Error.serviceUnavailable();
              }
              break;
            default:
              _error = Error.unexpectedError();
          }
        } else if (error is SocketException) {
          _error = Error.noInternetConnection();
        } else {
          _error = Error.unexpectedError();
        }
        return _error;
      } catch (_) {
        return Error.unexpectedError();
      }
    } else {
      return Error.unexpectedError();
    }
  }

  String get errorMessage => this.when(
      requestCancelled: () => "キャンセルされました",
      unauthorisedRequest: () => "認証エラーです",
      requestError: (apiError error) => error.message,
      serviceUnavailable: () => "しばらく時間をおいてから再度お試しください",
      sendTimeout: () => "通信環境の良いところで再度お試しください",
      noInternetConnection: () => "通信環境の良いところで再度お試しください",
      unexpectedError: () => "不明なエラーが発生しました"
  );
}
```

次に、`AppDio`というクラスを作成し、ここで`Dio`の設定を色々と行っています。

`app_dio.dart`

```dart
class AppDio with DioMixin implements Dio {
  factory AppDio() {
    if (_instance == null) {
      final dio = AppDio._();
      dio.httpClientAdapter = DefaultHttpClientAdapter();
      dio.options = BaseOptions(...);
      dio.interceptors
        ..add(LogInterceptor(responseBody: true));
      _instance = dio;
    }
    return _instance;
  }

  static AppDio _instance;
  AppDio._();
}
``` 

`APiClient`, `Dio`などを利用し、`DataSourceImpl`などから以下のようにリクエストを送ります。

```dart
class SomethingDataSourceImpl with SomethingDataSourceMixin {
  SomethingDataSourceImpl({ @required Dio dio }) : _dio = dio;

  final Dio _dio;

  @override
  Future<Result<Something>> fetchSomething() async {
    try {
      return await _dio.get<Map<String, dynamic>>("something").then((response) => 
        Result.success(Something.fromJson(json)));
    } catch(error) {
      return Result.failure(error);
    }
  }
}
```

上記のようにクラスやメソッドを用意することにより、`SomethingDataSourceImpl` からデータを受け取ったクラスでは、
レスポンスを以下のように処理することができます。

```dart
final Result<Something> result = await _somethingRepository.fetchSomething();
result.when(
  success: (Something something) {
    ...
  },
  failure: (Error error) {
    ...
  }
);
```

`result.` とうったら `when()` が予測に出てくると思います。

これによって、`kotlin` の `sealed class` のように満たしたかった HTTP Client のレスポンスnの型に依存せず、`when`文を利用し、`success`, `failure` を処理することができました。

# おまけ

dioのレスポンスをハンドルするクラスとして `ApiClient` というクラスを作成しました。
処理の内容は`AppDio`に書いても問題ないかもしれません。

`api_client.dart`

```dart
class ApiClient {
  factory ApiClient() {
    if (_instance == null) {
      _instance = ApiClient._();
    }
    return _instance;
  }
  static ApiClient _instance;
  ApiClient._();

  Future<Result<T>> sendRequest<T>({
    @required Future<Response<Map<String, dynamic>>> request,
    @required T Function(Map<String, dynamic>) jsonDecodeCallback
  }) async {
    try {
      return await request.then((value) => Result.success(jsonDecodeCallback(value.data)));
    } catch (error) {
      return Result.failure(Error.getApiError(error));
    }
  }
}
```

ここで鍵になるのは、引数で`request`と`jsonDecodeCallback`を受け取っていることです。

`request` は、dioを利用したリクエストそのもので、dioにはget(), post(), put() 等、様々なリクエストがありますが、それらのレスポンスの型が `Future<Response<T>>` となっていることを利用し、ここで集約します。`Future<Response<T>>` における `<T>` は then で流れてくる `Response` クラスのインスタンスが保持している `data` プロパティの型です。

これを `Map<String>` 十することで、`Response` クラスのインスタンスから `json` を受け取っています。

そして、受け取った `json` を本来ほしい型へ変換するために `jsonDecodeCallback` を用意しています。

`T Function(Map<String, dynamic>) jsonDecodeCallback` とすることで、`Map<String, dynamic>` を引数として `T` を返す `Function` を引数とすることができます。

これをやろうと思った背景は、上記の `SomethingDataSourceImpl`のように、`DataSource`層に毎回 `try-catch`、エラーハンドリングを書くことが煩わしかったため、作成しました。

```dart
try {
  return await _dio.get().then((response) => Result.success(Something.fromJson(response)));
} catch(error) {
  return Result.failure(Error.getApiError(error));
}
```

`SomethingDataSourceImpl` は以下のように書き換わります。

```dart
class SomethingDataSourceImpl with SomethingDataSourceMixin {
  SomethingDataSourceImpl({ @required Dio dio }) : _dio = dio;

  final Dio _dio;

  @override
  Future<Result<Something>> fetchSomething() async => ApiClient().sendRequest(
    request: _dio.get<Map<String, dynamic>>("something"),
    jsonDecodeCallback: (json) => Something.fromJson(json)
  );
}
```

スッキリしたように感じないでしょうか？
感じなかったら別にやる必要はないと思います。


# 終わりに

拙い文章でしたので、もし何か`dio`や`freezed`の使い方、`Result<T>`型のつくり方、ミスの指摘やフィードバック、質問等がありましたら、ぜひ [@muttsu_623](https://twitter.com/muttsu_623/) までご連絡ください。