OpenAPI x Flutterでプロダクト開発をより早く回そう
やること
この記事では下記のようなことができるようになります。
- OpenAPI Generatorを利用してAPIスキーマからclientとmodelを自動生成する
- Flutterプロジェクトにおいてその生成されたファイルを利用する
- UI側の開発が先行している状態において仮データを返すような実装を用意する
また前提としてAPIスキーマは既に存在していて swagger.yaml
を利用しますので
APIスキーマ自体の作成方法などについては触れません。
Open API Generatorについて
OpenAPI Generator を使用すると、 OpenAPI 仕様(2.0 と 3.0) を指定して、API クライアント ライブラリ (SDK 生成)、サーバー スタブ、ドキュメント、および構成を自動的に生成できます。
導入
まずは openapi-generator
をインストールしていきます。
インストール方法はいくつか用意されているのでお好みの方法でインストールしてください。
インストールする
brew install openapi-generator
インストールバージョンの確認をします。
この記事を書いている時点では 6.6.0
のバージョンを利用しています。
% openapi-generator --version
openapi-generator-cli 6.6.0
commit : 7f8b853
built : -999999999-01-01T00:00:00+18:00
source : https://github.com/openapitools/openapi-generator
docs : https://openapi-generator.tech/
APIスキーマのバリデーションしてみる
APIスキーマにエラーがないかチェックすることができるので下記のコマンドで確認してみましょう。
Erorrが出力されたら直しますが、Warningはあっても次に行うクライアント作成は成功します。
openapi-generator validate -i swagger.yaml
Client/Modelの生成
APIスキーマのエラーがないところまで進んでいたらあとはスキーマに沿ってdartのコードを自動生成していきます。
# dart ジェネレーターで openapi ディレクトリに生成する
openapi-generator generate -i swagger.yaml -g dart -o openapi
# dart-dio ジェネレータ(dart/libraries/dio)で openapi ディレクトリに生成する
openapi-generator generate -i swagger.yaml -g dart-dio -o openapi
# 生成した後に build_runnerを実行する(dartの場合は不要
cd openapi && flutter pub run build_runner build --delete-conflicting-outputs
どのような実装のdartコードを出力するかはジェネレータを選択できるようになっています。
dart
or dart-dio
の2種類から選択可能で dart
はhttpsパッケージをベースとしたAPI Client、 dart-dio
は dioパッケージをベースとしたAPI Clientコードが出力されます。
プロジェクト内では dio をラップしたApiClientを元々利用したので以降は設定などがそのまま利用できる dart-dio
を選択していきます。
テンプレートの詳細についてはそれぞれ下記で確認することができます。
また、テンプレートを少し編集して使いたい、テンプレート構成を大幅に変えて対応したいといった場合も対応は可能のようですが mustache
テンプレートの構造がメンテナンスしやすいとは思えなかったこととカスタムする必要性も今のところないので特に触れません。
プロジェクトの実装に組み込む
上記でopenapiディレクトリに出力した自動生成コードはパッケージ扱いとなるため、
pubspec.yaml
でインポートする設定を書く必要があります。
先に書いてしまいますが、出力されたモデルの中にListを扱うモデルがある場合は built_collection
に同梱されている ListBuilder
が必須となってくるため一緒に追加しておくと後で楽になります。
dependencies:
built_collection: ^5.1.1
openapi:
path: ./openapi
APIリクエストを送る
既にAPIが実装されていて利用可能である場合は実際に叩いてみましょう。
スニーカーの一覧を返すエンドポイントがあり、
その一覧を取得しにいくといった場合のAPIの利用のサンプルです。
SneakersApi
はパッケージ読み込みとなるため、プロジェクト内で利用している Dio
をProviderから読み込んで渡して実行するだけで動きます。
APIの結果も自動生成された SneakerResponse
型をそのまま利用できるため、API仕様をアプリエンジニアが確認して freezed
で定義してといった事前作業も不要になります。
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:openapi/openapi.dart';
class SneakerRepositoryImpl implements SneakerRepository {
const SneakerRepositoryImpl(this.ref);
final Ref ref;
Future<SneakersResponse> getSneakers() async => (await SneakersApi(
ref.read(dioProvider),
standardSerializers,
).sneakersGet())
.data!;
}
固定値を返したい場合
APIが実装中でアプリケーションは仮のデータで開発を進めたい場合もあると思います。
そういった場合に固定値を返すやり方です。
freezed
などに慣れているとコンストラクタで値を入れてやればいいかなと思うのですが、
生成されたモデルクラスには **Builder
を利用して値をセットしてあげる必要があります。
まず、そのモデルのプロパティが配列である場合には ListBuilder
に対して **Builder
で値をそれぞれおセットしていく必要があります。(ここで built_collection
が必要になる
下記がそのサンプルコードです。
import 'package:openapi/openapi.dart';
class SneakerInmemoryRepositoryImpl implements SneakerRepository {
Future<SneakersResponse> getSneakers() async {
return SneakersResponse(
(builder) {
builder.models = ListBuilder([
ViewSneakerModel((update) {
update
..id = '1'
..name = 'Nike Dunk'
..thumbnailUrl =
'https://some-assets.com/nike-dunk.png';
}),
ViewSneakerModel((update) {
update
..id = '2'
..name = 'Jordan 1'
..thumbnailUrl =
'https://some-assets.com/jordan-1.png';
}),
]);
},
);
}
これで実際にAPIにリクエストしたい場合は利用したいクラスにて SneakerRepositoryImpl
かあるいは固定値で実装を進めたい場合には SneakerInmemoryRepositoryImpl
をProviderで切り替えてあげることでスムーズに開発が進められそうです。
まとめ
Open API Generatorを使ってクライントコードを自動生成することでAPIスキーマを見てAPIクライアントやレスポンスのモデル定義を行う必要がなくなり、パスの書き間違い・キーの書き間違いといった細かなミスも防ぐことができるようになりました。
また、API仕様書を見てfreezedクラスをぽちぽち作っていく作業が不要になったことでUIの実装やビジネスロジックなどの本来注力したいところに時間をかけられることも良い点かなと感じました。
今回 OpenAPI Generatorを使ってみたことで以下のような課題も見つかったのでさらに改善をしていきたいと思います。
- APIスキーマにvalidation errorがない状態をどう保守していくか?
- バックエンドとアプリのリポジトリは別々なのでAPIスキーマをどう連携していくか?
- APIクライアント/モデルのテストはどのよう・どこに書くか?
株式会社SODAの開発組織がお届けするZenn Publicationです。 是非Entrance Bookもご覧ください! → recruit.soda-inc.jp/engineer
Discussion