OpenAPI x Flutterでプロダクト開発をより早く回そう

by dicekest,

やること

  • OpenAPI Generatorを利用してAPIスキーマからclientとmodelを自動生成する
  • Flutterプロジェクトにおいてその生成されたファイルを利用する
  • UI側の開発が先行している状態において仮データを返すような実装を用意する

また前提としてAPIスキーマは既に存在していて swagger.yaml を利用しますので APIスキーマ自体の作成方法などについては触れません。

Open API Generatorについて

OpenAPI Generator を使用すると、 OpenAPI 仕様(2.0 と 3.0) を指定して、API クライアント ライブラリ (SDK 生成)、サーバー スタブ、ドキュメント、および構成を自動的に生成できます。

https://github.com/OpenAPITools/openapi-generator

導入

まずは openapi-generator をインストールしていきます。 インストール方法はいくつか用意されているのでお好みの方法でインストールしてください。 https://openapi-generator.tech/docs/installation/

インストールする

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 テンプレートの構造がメンテナンスしやすいとは思えなかったこととカスタムする必要性も今のところないので特に触れません。 https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/dart2 https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/dart/libraries/dio

プロジェクトの実装に組み込む

上記で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;

  @override
  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 {
  @override
  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クライアント/モデルのテストはどのよう・どこに書くか?