【Flutter】HTTPクライアントパッケージ「Chopper」でAPI通信をする

# Chopperとは

Chopperとは、Dart製のHTTPクライアントのラッパー的な便利パッケージです。内部的には、公式のhttpパッケージを使ってAPI通信をしています。
また、AndroidのRetrofitのようにインターフェースを定義しただけでクライアントを呼び出す部分の実装を自動生成してくれるので、とても便利です。今回は、この「Chopper」を使ってAPI通信をする方法について簡単に解説していきます。
プラグインページは以下です。

https://pub.dev/packages/chopper

# 環境

以下の環境を想定しています。
<img width="634" alt="スクリーンショット 2020-01-19 16.43.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/147934/7695ddad-0a22-ec39-7a1d-3c2c1653f393.png">

# 1. プラグインのインストール

まずは、インストールです。`pubspec.yaml`を編集します。執筆時点で最新のバージョンを指定しています。

```pubspec.yml
name: 
description: 

...

dependencies:
  flutter:
    sdk: flutter
  ...
  chopper: ^3.0.1+1 // Add

dev_dependencies:
  flutter_test:
    sdk: flutter
  ...
  build_runner: ^1.7.3  // Add
  chopper_generator: ^3.0.1+1  // Add
```

# 2. インターフェースを定義する

ますここで必要なのは、

- `part`で自動生成用コードのパスを指定する。
- `@ChopperApi()`アノテーションを付与
- `ChopperService`を継承させたabstractクラスにする
- `create()`メソッドを定義

その後、エンドポイントをメソッドとして定義します。

```service.dart
import 'package:chopper/chopper.dart';

part 'vegetable_service.chopper.dart';

@ChopperApi(baseUrl: '/v1')
abstract class VegetableService extends ChopperService {
  static VegetableService create([ChopperClient client]) =>
      _$VegetableService(client);
  
   ...
}
```

### 使用できるHTTPメソッド

HTTPメソッドは、以下がアノテーションで使用可能です。

```
@Get
@Post
@Delete
@Put
@Patch
@Head
```

例えば以下のようになります。

```service.dart
import 'package:chopper/chopper.dart';

part 'vegetable_service.chopper.dart';

@ChopperApi(baseUrl: '/v1')
abstract class VegetableService extends ChopperService {
  static VegetableService create([ChopperClient client]) =>
      _$VegetableService(client);

  @Get(path: "/vegetables")
  Future<Response> fetchVegetables({
    @Query('page[number]') int pageNumber = 1,
  });

  @Get(path: "/vegetables/{vegetable_id}")
  Future<Response> fetchVegetables({
    @Path('vegetable_id') int vegetableId,
  });

  @Post(path: '/vegetables')
  Future<Response> postVegetable(@Body() VegetableForm vegetableForm);
}
```
### urlのpathにパラメータを指定する

`@Get(path: "/vegetables/{vegetable_id}")`のように波括弧でまずパラメータ名を決めます。

`@Path()`アノテーションを使って、{}内のnameと`@Path()`のパラメータを一致させます。

```service.dart
@Get(path: "/vegetables/{vegetable_id}") 
Future<Response> fetchVegetables({
  @Path('vegetable_id') int vegetableId,  // 上の{vegetable_id}と@Path('vegetable_id')を一致させる。
});
```

### クエリストリングにパラメータを指定する

`@Query()`アノテーションを使って、メソッドの引数に、指定したいパラメータを指定します。

```service.dart
@Get(path: "/vegetables")
Future<Response> fetchVegetables({
  @Query('page[number]') int pageNumber = 1,
});
```

### リクエストボディを設定する

`@Body()`アノテーションを使って、メソッドの引数にリクエストボディとして送信したいモデルを指定します。

```service.dart
@Post(path: '/vegetables')
Future<Response> postVegetable(
  @Body() VegetableForm vegetableForm,
);
```

# 3. 自動生成コマンドを実行

以下のコマンドをFlutterプロジェクト直下で実行すると、2で定義したインターフェースに基づいて、実装ファイルが生成されます。
出力されるファイル名は`part`で指定した名前になります。

```
$ flutter packages pub run build_runner build --delete-conflicting-outputs
```

# 4. ChopperClientを生成

採用しているアプリのアーキテクチャにもよりますが、RepositoryなどのAPIコールをしたいエンドポイントで、`ChopperClient`を初期化します。私が今作っているアプリでは、ChopperClientCreatorというChopperClientを生成するクラスを作り、Repositoryで生成するようにしています。

```chopper_client_creator.dart
class ChopperClientCreator {
  static final String baseUrl = "https://hogefuga.xyz";

  static ChopperClient create() {
    return ChopperClient(
      baseUrl: ChopperClientCreator.baseUrl,
      converter: JsonConverter(),
    );
  }
}
```

Repositoryにて、ChopperClientCreatorを経由してChopperClientを初期化します。

```repository.dart
class Repository {
  final VegetableService service = 
 VegetableService.create(ChopperClientCreator.create());
  
  ...
}
```

ここでは、あまりテスタビリティは考えていません。。

# 5. APIコール&jsonのパース

Repositoryにて、Serviceを利用して、APIをコールします。

```repository.dart
class Repository {
  final VegetableService service = 
 VegetableService.create(ChopperClientCreator.create());
  
  Future<List<Vegetable>> fetchVegetables() async {
    final Response response = await service.fetchVegetables();
    if (response.isSuccessful) {
      final responseBodyJson = response.body as Map<String, dynamic>;
      // jsonのパース
    }
  }
}
```

# 補足

## Converterの設定

上記では触れていないのですが、ChopperClient初期化時にConverterを指定することができます。
これを指定すると、こちらで定義したデータ型で、レスポンスを返してくれるようになります。

Chopperでは以下のconverterがデフォルトで用意されています。

- JsonConverter
- FormUrlEncodedConverter

ConverterはChopperClientの生成時に指定します。

```client_creator.dart
ChopperClient(
      baseUrl: ChopperClientCreator.baseUrl,
      converter: JsonConverter(), // Add
    );
```

### JsonConverter

こちらを指定すると以下のような`Response`クラスや`Request`クラス(コードは割愛)にパースしてくれます。

```Response.dart
class Response<BodyType> {
  final http.BaseResponse base;
  final BodyType body;

  Response(this.base, this.body);

  Response replace<NewBodyType>({http.BaseResponse base, NewBodyType body}) =>
      Response<NewBodyType>(base ?? this.base, body ?? this.body);

  int get statusCode => base.statusCode;

  bool get isSuccessful => statusCode >= 200 && statusCode < 300;

  Map<String, String> get headers => base.headers;

  Uint8List get bodyBytes =>
      base is http.Response ? (base as http.Response).bodyBytes : null;

  String get bodyString =>
      base is http.Response ? (base as http.Response).body : null;
}
```

### カスタムのConverter

カスタムでConverterを設定したければ、以下のConverterを継承したクラスを実装すれば設定できると思います。

```Converter.dart
@immutable
abstract class Converter {
  FutureOr<Request> convertRequest(Request request);

  /// [BodyType] is the expected type of your response
  /// ex: `String` or `CustomObject`
  ///
  /// In the case of [BodyType] is a `List` or `BuildList`
  /// [InnerType] will be the type of the generic
  /// ex: `convertResponse<List<CustomObject>, CustomObject>(response)`
  FutureOr<Response<BodyType>> convertResponse<BodyType, InnerType>(
      Response response);
}
```

## ErrorConverterの設定

こちらはリクエストがErrorだった際に使用されるコンバータです。JsonConverterと同様に以下が利用できます。

- JsonConverter
- FormUrlEncodedConverter

上記のようなErrorConverterはChopperClientの生成時に指定します。

```client_creator.dart
ChopperClient(
      baseUrl: ChopperClientCreator.baseUrl,
      converter: JsonConverter(),
      errorConverter: JsonConverter(),  // Add
    );
```

### カスタムのErrorConverter

カスタムでErrorConverterを設定したければ、以下のErrorConverterを継承したクラスを実装すれば設定できると思います。

```ErrorConverter.dart
abstract class ErrorConverter {
  FutureOr<Response> convertError<BodyType, InnerType>(Response response);
}
```

## interceptorの設定

interceptorを利用すると、主に以下の二つができます。

- HTTPHeaderの指定
- Logging

### HTTPHeaderの指定

HTTPヘッダーを設定するには、ChopperClient生成時に、interceptorsのプロパティを使って設定します。

```client_creator.dart
ChopperClient(
          baseUrl: BlaboClientCreator.baseUrl,
          converter: JsonConverter(),
          errorConverter: JsonConverter(),
          interceptors: [ // Add
            (Request request) async => request.replace(
                  headers: {
                    'Content-Type': 'application/json',
                    "Authorization": 'abcdefghijkhogehoge',
                    "Accept-Encoding": "gzip",
                  },
                ),
          ],
        );
```

また、chopperには以下のような定数が用意されているので、使えるものは使うと良さそうです。

```constants.dart
const contentTypeKey = 'content-type';
const jsonHeaders = "application/json";
const formEncodedHeaders = "application/x-www-form-urlencoded";
```

定数を使用するとこのような使い方になります。

```chopper_client.dart
ChopperClient(
          baseUrl: BlaboClientCreator.baseUrl,
          converter: JsonConverter(),
          errorConverter: JsonConverter(),
          interceptors: [ // Add
            (Request request) async => request.replace(
                  headers: {
                    contentTypeKey: contentType, // 定数を使用
                    "Authorization": 'abcdefghijkhogehoge',
                    "Accept-Encoding": "gzip",
                  },
                ),
          ],
        );
```

### Logging

interceptorsを使うと、HTTPリクエストのロギングもできます。以下のように記述するとログに通信内容を出力することができます。

```client_creator.dart
ChopperClient(
          ...
          interceptors: [
            (Request request) async => request.replace(
                  headers: {
                    contentTypeKey: contentType,
                    "User-Agent": "", // TODO: UserAgentの取得,
                    "Authorization": await getAccessToken(),
                    "Accept-Encoding": "gzip",
                  },
                ),
            (Request request) async {  // Add （Requestのロギング）
              debugPrint("""
                =========HTTP Request logging=========
                baseUrl: ${request.baseUrl}
                url: ${request.url}
                parameter: ${request.parameters}
                method: ${request.method}
                headers: ${request.headers}
                body: ${request.body}
                multipart: ${request.multipart}
                parts: ${request.parts}
                ======================================
              """);
              return request;
            },
            (Response response) async {  // Add （Responseのロギング）
              debugPrint("""
                =========HTTP Response logging=========
                url: ${response.base.request.url}
                status: ${response.statusCode}
                headers: ${response.headers}
                body: ${response.body}
                ======================================
              """);
              return response;
            },
          ],
        );
```

# まとめ

Chopperの日本語の記事が少なかったので、今回取り上げてみました。AndroidのRetrofitやOkHttpと利用方法が似ているので、個人的には便利なのではと思います。