Flutter : chopperでの通信基盤作成

# こちらも合わせてご覧ください 
[Flutter通信ライブラリ選定 ~ 選ばれたのはRetrofitでした ~](https://qiita.com/Morisan/items/9688ab92ff94024353c5)

## 簡単な特徴
1. 新しいAPIごとに作成する必要があるのは、
・Repositoryのクラス（メソッド）
・RequestParameterクラス
・Response受け取るクラス
2. Converterで無理やりconvertしてあげなきゃいけない
3. httpのラッパーライブラリ

# 実装
特徴を書くよりもコードを書いた方がわかりやすいと思うので、
RepositoryからAPIを呼び出しレスポンスオブジェクトを返すところまで記載します。
###### 前提
1. pubspec.yamlに`freezed`と`freezed_annotation`と`json_serializable`と`build_runner`を固定で入れておいてください。
2. 叩くAPIとしては、下記のようなログインAPIを例として挙げます
・Url →  `https://domain-hoge.com/fuga/piyo/login`
・Method → `POST`
・Header → `{ Content-Type: "applicatioin/json", "piyo": "hogehoge" }`
・BodyParameter → `{ id: String, pass: String}`
・ResponseJson → `{ base: { bar: String, toto: String } }`（レスポンスにbaseなどのキーが固定で入っている場合を考慮）
3. chopperをpubspec.yamlに追記する

### 具体的実装
クライアント生成　必要なInterceptorとConvertor
```dart:client.dart
class Client extends ChopperClient {
  KDClient()
      : super(converter: CustomConvertor(), interceptors: [CustomHeadersInterceptor.create()]);

  @override
  String get baseUrl => "https://domain-hoge.com";
}

class CustomConvertor implements Converter {
  @override
  Request convertRequest(Request request) {
    final req = applyHeader(
      request,
      contentTypeKey,
      jsonHeaders,
      override: false,
    );

    return encodeJson(req);
  }

  @override
  FutureOr<Response<BodyType>> convertResponse<BodyType, InnerType>(
      Response response) {
    return decodeJson<BodyType, InnerType>(response);
  }

  Request encodeJson(Request req) {
    var contentType = req.headers[contentTypeKey];
    if (contentType != null && contentType.contains(jsonHeaders)) {
      return req.copyWith(body: json.encode(req.body));
    }
    return req;
  }

  Response<BodyType> decodeJson<BodyType, InnerType>(Response response) {
    var contentType = response.headers[contentTypeKey];
    var body = response.body;
    if (contentType != null && contentType.contains(jsonHeaders)) {
      body = utf8.decode(response.bodyBytes);
    }
    try {
      var mapData = json.decode(body);
      var responseBase = mapData["base"];
      BodyType bodyType;
      switch (BodyType) {
        case LoginResponse:
          bodyType = LoginResponse.fromJson(data as Map<String, dynamic>) as BodyType;
          break;
        case String:
          bodyType = responseBase.data as BodyType;
          break;
        default:
          bodyType = responseBase as BodyType;
      }

      return response.copyWith<BodyType>(body: bodyType);
    } catch (e) {
      chopperLogger.warning(e);
      return response.copyWith<BodyType>(body: body);
    }
  }
}

```
RepositoryからClient.callを呼び出す
```dart:login_repository.dart
abstract class LoginDataSource {
  Future<Result<LoginResponse>> login(LoginModel loginModel);
}

class LoginRepository extends LoginDataSource {
  LoginRepository() : service = AuthService.create(Client());
  AuthService service;

  @override
  Future<Result<LoginResponse>> login(LoginModel loginModel) async {
    final parameter = LoginRequestParameter(
        id: loginModel.id, pass: loginModel.pass);
    try {
      final response = await service.login(parameter);
      return Result.success(response.body!);
    } catch (e) {
      return Result.failure();
    }
  }
}
```
リクエストを叩くChopperServiceクラスの作成
下記クラスを作成したらgenerateしましょう
```dart:auth_service.dart
part 'auth_service.chopper.dart';

@ChopperApi(baseUrl: "/fuga/piyo")
abstract class AuthService extends ChopperService {
  static AuthService create([ChopperClient? client]) => _$AuthService(client);

  @Post(path: "/login")
  Future<Response<LoginResponse>> login(@Body() LoginRequestParameter parameter);
}
```
リクエスト等の基幹クラス
```dart:http_request_base.dart
// リクエストの基幹
abstract class HttpRequestBase {
  HttpMethod get apiMethod;
  String get path;
  RequestParameter? get parameter;

  String get method => apiMethod.name.toUpperCase();

  Uri get url {
    if (apiMethod == HttpMethod.get && parameter != null) {
      return Uri.https("domain-hoge.com", path, parameter!.toJson());
    }
    return Uri.https("domain-hoge.com", path);
  }

  Map<String, String> get headers => { Content-Type: "applicatioin/json", "piyo": "hogehoge" };
}

enum HttpMethod {
  post,
  get,
  put,
  patch,
  delete
}
```
リザルトクラス（上記同様にflutter pub run build_runner build ~ する）
```dart:result.dart
part 'result.freezed.dart';

@freezed
class Result<T> with _$Result<T> {
  const factory Result.success(T value) = Success<T>;
  const factory Result.failure() = Failure<T>;
}
```
ログインリクエスト
```dart:login_request.dart
class LoginRequest extends HttpRequestBase {
  LoginRequest(this.loginParameter) : super();
  
  final LoginRequestParameter loginParameter;
  
  @override
  HttpMethod get apiMethod => HttpMethod.post;

  @override
  String get path => "/fuga/piyo/login";

  @override
  RequestParameter? get parameter => loginParameter;
}
```
ログインリクエストのパラメータ（freezedなので下記ファイルを作成したら、$`flutter pub run build_runner build --delete-conflicting-outputs`を実行する）
```dart:login_request_parameter.dart
part 'login_request_parameter.freezed.dart';
part 'login_request_parameter.g.dart';

@freezed
class LoginRequestParameter with _$LoginRequestParameter {
  factory LoginRequestParameter({
    required String id,
    required String pass,
  }) = _LoginRequestParameter;

  factory LoginRequestParameter.fromJson(Map<String, dynamic> json) => _$LoginRequestParameterFromJson(json);
}
```
レスポンス受け取るクラス（上記同様にflutter pub run build_runner build ~ する）
```dart:login_response.dart
part 'login_response.freezed.dart';
part 'login_response.g.dart';

@freezed
class LoginResponse with _$LoginResponse {
  factory LoginResponse({
    required String bar,
    required String toto,
  }) = _LoginResponse;

  factory LoginResponse.fromJson(Map<String, dynamic> json) => _$LoginResponseFromJson(json);
}
```