Chopperとは
Chopperとは、Dart製のHTTPクライアントのラッパー的な便利パッケージです。内部的には、公式のhttpパッケージを使ってAPI通信をしています。
また、AndroidのRetrofitのようにインターフェースを定義しただけでクライアントを呼び出す部分の実装を自動生成してくれるので、とても便利です。今回は、この「Chopper」を使ってAPI通信をする方法について簡単に解説していきます。
プラグインページは以下です。
環境
1. プラグインのインストール
まずは、インストールです。pubspec.yaml
を編集します。執筆時点で最新のバージョンを指定しています。
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()
メソッドを定義
その後、エンドポイントをメソッドとして定義します。
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
例えば以下のようになります。
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()
のパラメータを一致させます。
@Get(path: "/vegetables/{vegetable_id}")
Future<Response> fetchVegetables({
@Path('vegetable_id') int vegetableId, // 上の{vegetable_id}と@Path('vegetable_id')を一致させる。
});
クエリストリングにパラメータを指定する
@Query()
アノテーションを使って、メソッドの引数に、指定したいパラメータを指定します。
@Get(path: "/vegetables")
Future<Response> fetchVegetables({
@Query('page[number]') int pageNumber = 1,
});
リクエストボディを設定する
@Body()
アノテーションを使って、メソッドの引数にリクエストボディとして送信したいモデルを指定します。
@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で生成するようにしています。
class ChopperClientCreator {
static final String baseUrl = "https://hogefuga.xyz";
static ChopperClient create() {
return ChopperClient(
baseUrl: ChopperClientCreator.baseUrl,
converter: JsonConverter(),
);
}
}
Repositoryにて、ChopperClientCreatorを経由してChopperClientを初期化します。
class Repository {
final VegetableService service =
VegetableService.create(ChopperClientCreator.create());
...
}
ここでは、あまりテスタビリティは考えていません。。
5. APIコール&jsonのパース
Repositoryにて、Serviceを利用して、APIをコールします。
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の生成時に指定します。
ChopperClient(
baseUrl: ChopperClientCreator.baseUrl,
converter: JsonConverter(), // Add
);
JsonConverter
こちらを指定すると以下のようなResponse
クラスやRequest
クラス(コードは割愛)にパースしてくれます。
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を継承したクラスを実装すれば設定できると思います。
@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の生成時に指定します。
ChopperClient(
baseUrl: ChopperClientCreator.baseUrl,
converter: JsonConverter(),
errorConverter: JsonConverter(), // Add
);
カスタムのErrorConverter
カスタムでErrorConverterを設定したければ、以下のErrorConverterを継承したクラスを実装すれば設定できると思います。
abstract class ErrorConverter {
FutureOr<Response> convertError<BodyType, InnerType>(Response response);
}
interceptorの設定
interceptorを利用すると、主に以下の二つができます。
- HTTPHeaderの指定
- Logging
HTTPHeaderの指定
HTTPヘッダーを設定するには、ChopperClient生成時に、interceptorsのプロパティを使って設定します。
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には以下のような定数が用意されているので、使えるものは使うと良さそうです。
const contentTypeKey = 'content-type';
const jsonHeaders = "application/json";
const formEncodedHeaders = "application/x-www-form-urlencoded";
定数を使用するとこのような使い方になります。
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リクエストのロギングもできます。以下のように記述するとログに通信内容を出力することができます。
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と利用方法が似ているので、個人的には便利なのではと思います。