モックサーバを使ったFlutterのアプリ開発環境を作る

## はじめに

株式会社MG-DXでWebフロントエンド兼Flutterアプリエンジニアを業務を行なっています。自社のサービスとして、医療機関、薬局向けに薬急便のサービスを提供しています。

薬急便では、OpenAPIを使ったスキーマ駆動開発をしており、

開発の流れとしては、
 1. 仕様決め
 1. 仕様を元にバックエンド側データの定義とAPI設計
 1. バックエンドがOpenAPIを定義
 1. フロント、バックエンド共に開発に入る
 1. 結合テスト

となっています。バックエンドが定義した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](https://melos.invertase.dev/)を利用します。

```yaml
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

```package.json
{
  "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つの分けていますが、複数のサービスが扱えるように、このような構成にしています。

```JSON
{
  "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配下にファイルが生成されるようになります。

```melos.yaml
  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:*をまとめて実行します。


```JSON
{
  "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を取得して、設定するように書き換えます。

```JSON
{
  "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`のパッケージを使って、ビルド設定ごとにアドレスを指定できるようにします。

```todo_repository.dart
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);
  }
}
```

```main_mock.dart
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`と指定するとパッケージの雛形でプロジェクトが作成されます。

```shell
fvm flutter create --template=package packages/mock_setting
```

今回アノテーションを使った書き出しにするので、build_runner内でsettingを定義するPartBuilderを作成します。GeneratorForAnnotationを使うと、アノテーションに対応したBuilderが作成できるので、これを継承して、build_runnerを作成します。

### アノテーションの定義

アノテーションの定義は以下になります。これで、@GenerateIpSetting(name, value)とアノテーションを定義できます。

```annotation.dart
class GenerateIpSetting {
  final String name;
  final String value;

  const GenerateIpSetting(this.name, this.value);
}
```

### build_runner本体
generateForAnnotatedElementの戻り値に今回のGenerateIpSettingに対応した内容を返してやると、対応したdartファイルを生成してくれます。

```dart
class MockSettingGenerator extends GeneratorForAnnotation<GenerateIpSetting> {
  @override
  FutureOr<String> generateForAnnotatedElement(
      Element element, ConstantReader annotation, BuildStep buildStep) async {
  }
}
```

### generate内でIPアドレスを取得する

IPアドレスの取得する処理をProcessを介して実行して受け取るようにします。shellだと1行になるのですが、複数のコマンドを実行して繋げていくので、このような処理になります。

```dart
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を書き換えるようにします。

```main_mock.dart
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を実行するコマンドを追加して、簡単に呼び出せるようにします。

```melos.yaml
  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を書き出します。

```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