はじめに
株式会社MG-DXでWebフロントエンド兼Flutterアプリエンジニアを業務を行なっています。自社のサービスとして、医療機関、薬局向けに薬急便のサービスを提供しています。
薬急便では、OpenAPIを使ったスキーマ駆動開発をしており、
開発の流れとしては、
- 仕様決め
- 仕様を元にバックエンド側データの定義とAPI設計
- バックエンドがOpenAPIを定義
- フロント、バックエンド共に開発に入る
- 結合テスト
となっています。バックエンドが定義したOpenAPIを元にモデルやAPIの呼び出しを簡単に書き出しできるので、フロント開発では、呼び出しのタイミングと画面の処理ができれば回るので、早く開発できます。
ここからが本題です。
バックエンドが先行して開発しているので、フロントが作業に入るころにはAPIはできあがっているのですが、ない場合もあります。その場合には、モックサーバを利用して開発しています。今回は、モックサーバを使った開発方法について説明します。
この記事で説明する内容
今回の大きく分けると3つの構成になります。簡単なTodoアプリの例に説明を進めます。
- OpenAPIを使ったスキーマ駆動開発環境を作る
- モックサーバの利用
- build_runnerの作成
こちらに実際に作成した内容を公開しているので、記事と合わせて見ていただくといいかなと思います。
https://github.com/ueki-tomohiro/build_network_setting
OpenAPIを使ったスキーマ駆動開発環境を作る
プロジェクトの構成
今回のプロジェクトは複数のパッケージを利用して使う構成になるので、ルート直下にパッケージを置かず、packagesの配下にすべてのパッケージを置くようにします。
/packages
┣app # アプリ本体
┣api # OpenAPIで書き出されたAPIの定義
┗mock_setting # モックサーバ用の定義を書き出すbulld_runner
複数のパッケージを管理することになるので、melosを利用します。
name: build_network_setting
sdkPath: .fvm/flutter_sdk
packages:
- packages/app/*
- packages/api/**
- packages/mock_setting/*
Node.jsの開発環境を用意する
OpenAPIのスキーマ駆動開発ためにNode.jsの開発環境を用意します。Flutterのプロジェクト直下にpackage.jsonを置いて、OpenAPI必要なパッケージを追加します。
- OpenAPIからFlutterに必要な定義を書き出すopenapi-generator
- OpenAPIのモックサーバを起動するprism
- 複数のOpenAPIのモックサーバを起動できるようにnpm-run-all
- OpenAPIのバリデーションを行うswagger-cli
{
"name": "build_network_setting",
"version": "0.0.0",
"private": true,
"scripts": {
},
"dependencies": {},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2",
"@stoplight/prism-cli": "^4.10.5",
"npm-run-all": "^4",
"swagger-cli": "^4.0.4"
}
}
OpenAPIの書き出し設定を追加
OpenAPIの定義からFlutterのコードを吐き出すためのscriptを追加します。3つの分けていますが、複数のサービスが扱えるように、このような構成にしています。
{
"scripts": {
"gen:api": "run-p --print-label --print-name generate-api:*",
"generate-api:todo": "SERVICE_NAME=external API_NAME=todo yarn generate-openapi",
"generate-openapi": "openapi-generator-cli generate -t ./document/api/templates -g dart -i ./document/api/services/$SERVICE_NAME/$API_NAME/openapi.yaml -o ./packages/api/$SERVICE_NAME/$API_NAME --global-property apis,apiDocs=false,apiTests=false,models,modelDocs=false,modelTests=false,supportingFiles --additional-properties=pubLibrary=$SERVICE_NAME.$API_NAME.api,pubName=${SERVICE_NAME}_${API_NAME}",
},
}
それぞれの用途は以下です。
generate-openapi
では、APIの定義自体は、document/api/searvices配下に置くことを想定し、serviceとapiを指定して書き出せるようにしました。
genarete-api:todo
は、generate-openapiを呼び出して、externalとtodoを渡して、TodoのサービスのAPIを書き出します。
gen:api
は、複数のサービスを同時に書き出せるようにgenarete-api:*をまとめて実行します。
melosから呼び出せるようにする
melosからAPIの書き出しとフォーマットを行うようにコマンドを追加します。実行すると、packages/api配下にファイルが生成されるようになります。
format:
run: fvm flutter format --fix packages
api:
run: |
yarn gen:api
melos format
OpenAPIの定義のもとにモックサーバを起動する
作成されたAPIをもとにアプリ開発ができれば、次に動作確認です。ここで、モックサーバを使って確認できるようにします。OpenAPIの書き出しと同じように複数のサービスを扱えるように3つのscriptを定義しています。
mock-openapi
では、APIの定義自体はdocument/api/searvices配下に置くことを想定し、serviceとapi、portを指定すると、prismを使ってモックサーバを起動します。
mock:todo
は、mock-openapiを呼び出して、externalとtodoを渡して、Todoのモックサーバを起動します。
start-mock
は、複数のサービスを同時に動かせるようにmock:*をまとめて実行します。
{
"scripts": {
"mock:todo": "SERVICE_NAME=external API_NAME=todo PORT=4010 yarn mock-openapi",
"mock-openapi": "prism mock --port=${PORT} ./document/api/services/${SERVICE_NAME}/${API_NAME}/openapi.yaml",
"start-mock": "run-p --print-label --print-name mock:*"
},
}
localhostではなくローカルネットワークのIPに変更する
ローカル環境内で呼び出して使えるのですが、シミュレータや端末からは参照できるようにローカルネットワークのIPで起動できるようにします。hostにIPアドレスを指定するのですが、今のローカルIPを取得して、設定するように書き換えます。
{
"scripts": {
"mock-openapi": "prism mock --host $(ifconfig -l | xargs -n1 ipconfig getifaddr | sed -n -e 1p) --port=${PORT} ./document/api/services/${SERVICE_NAME}/${API_NAME}/openapi.yaml",
},
}
端末やシミュレータからIPアドレスを指定して、モックサーバを参照できるようにする
OpenApiGeneratorで書き出されるApiClientではbasePathにサーバのアドレスを指定します。ここで、flutter_flavor
のパッケージを使って、ビルド設定ごとにアドレスを指定できるようにします。
class TodoRepository implements ITodoRepository {
final ITokenRepository tokenRepository;
late TodoApi _todoApi;
TodoRepository(this.tokenRepository) {
final apiClient = ApiClient(
basePath: FlavorConfig.instance.variables['todo-api'] as String,);
apiClient.addDefaultHeader('x-app-version',
FlavorConfig.instance.variables['app-version'] as String);
apiClient.addDefaultHeader(
'x-app-name',
FlavorConfig.instance.variables['app-name'] as String,
);
_todoApi = TodoApi(apiClient);
}
}
import 'package:app/app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_flavor/flutter_flavor.dart';
import 'package:mock_setting/annotation.dart';
Future<void> main() async {
FlavorConfig(
name: 'dev',
location: BannerLocation.bottomStart,
variables: <String, dynamic>{
'todo-api': 'http://192.168.0.***:4010',
'app-version': '0.0.0',
'app-name': 'todo',
});
await runMyApp();
}
build_runnerを使って、IPアドレスの可変に対応する
これで、端末やシミュレータからモックサーバが参照されるようになりますが、IPアドレスは作業する端末によって変わるので埋め込むことができません。そこで、build_runnerを使って、開発環境のIPアドレスを埋め込めるbuild_runnerを作成します。
mockを起動するmain_mock.dartに自作アノテーションを追加して、build_runner内で取得したIPアドレスを埋め込めるようにします。
build_runner用のパッケージを作成する
flutterのコマンドで作成するときに、--template=package
と指定するとパッケージの雛形でプロジェクトが作成されます。
fvm flutter create --template=package packages/mock_setting
今回アノテーションを使った書き出しにするので、build_runner内でsettingを定義するPartBuilderを作成します。GeneratorForAnnotationを使うと、アノテーションに対応したBuilderが作成できるので、これを継承して、build_runnerを作成します。
アノテーションの定義
アノテーションの定義は以下になります。これで、@GenerateIpSetting(name, value)とアノテーションを定義できます。
class GenerateIpSetting {
final String name;
final String value;
const GenerateIpSetting(this.name, this.value);
}
build_runner本体
generateForAnnotatedElementの戻り値に今回のGenerateIpSettingに対応した内容を返してやると、対応したdartファイルを生成してくれます。
class MockSettingGenerator extends GeneratorForAnnotation<GenerateIpSetting> {
@override
FutureOr<String> generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) async {
}
}
generate内でIPアドレスを取得する
IPアドレスの取得する処理をProcessを介して実行して受け取るようにします。shellだと1行になるのですが、複数のコマンドを実行して繋げていくので、このような処理になります。
final ifconfig = await Process.start('ifconfig', ['-l']);
final xargs = await Process.start('xargs', ['-n1', 'ipconfig', 'getifaddr']);
await ifconfig.stdout.pipe(xargs.stdin);
final sed = await Process.start('sed', ['-n', '-e', '1p']);
await xargs.stdout.pipe(sed.stdin);
await sed.stdout.transform(utf8.decoder).forEach((element) {
host = element.trimRight();
アノテーションをmain_mock.dartに定義
build_runnerで生成されるファイルを参照するようにして、FlavorConfigを書き換えるようにします。
import 'package:app/app.dart';
import 'package:flutter/material.dart';
import 'package:flutter_flavor/flutter_flavor.dart';
import 'package:mock_setting/annotation.dart';
part 'main_mock.mock_setting.dart';
@GenerateIpSetting('todo-api', 'http://__ip_addr__:4010')
Future<void> main() async {
FlavorConfig(
name: 'dev',
location: BannerLocation.bottomStart,
variables: <String, dynamic>{
...setting,
'app-version': '0.0.0',
'app-name': 'todo',
});
await runMyApp();
}
melosにbuild_runnerを実行するコマンドを追加して、簡単に呼び出せるようにします。
build_runner:
exec: fvm flutter packages pub run build_runner build --delete-conflicting-outputs
select-package:
depends-on: 'build_runner'
これで、melos build_runner
を実行すれば、IPアドレスを含んだmain_mock.mock_setting.dartを書き出します。
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'main_mock.dart';
// **************************************************************************
// MockSettingGenerator
// **************************************************************************
final setting = <String, String>{
'todo-api': 'http://192.168.0.***:4010',
};
まとめ
Node.js環境を用意し、build_runnerを使ってFlutterの開発環境を整えて開発しやすくしました。Node.js環境は、OpenAPIやモックサーバだけでなく、いろいろと提供されているので、取り込んでいけると思います。build_runnerは今回の使い方だと、TypeScriptで生成する処理を書き出した方が楽だと思います。
公開しているリポジトリでは動くところまで作っているので、より詳細にソースを見たい方は参考にしてください。
https://github.com/ueki-tomohiro/build_network_setting