つばくろぐ @takamii228

知は力なり

Flutterで使うAPI関連のDartコードをSwagger/OpenAPIで自動生成する

はじめに

Flutterの開発の中でSwagger/OpenAPIのYamlからコードを自動生成して使うことがあったのでまとめておきます。

Swagger/OpenAPIとは

OpenAPIはREST APIの仕様を記述するフォーマットで、yaml形式で仕様を定義することができます。

2.0系と3.0系があって、3.0が出てからしばらく立ちましたが、API Gatewayはまだ2.0系のみサポートしているところが多い印象です。

www.openapis.org

swagger.io

SwaggerとOpenAPIの関係は少しややこしいようで、加えて2.0系と3.0系で仕様に差分があるため、ここでは2.0系の形式を扱うこととします。

Swagger/OpenAPIには以下の3つのツールがあります。

今回はSwagger/OpenAPIのyamlを使ってAPI仕様書を見る手順と、Flutterで使うDartのコードを生成する手順をまとめます。

Swagger UIでAPI仕様書を見る

Swagger UIはDocker Imageが用意されているので、yamlを連携してあげれば簡単に起動することができます。

API_URLでデフォルトで読み込むyamlを指定できるので、マウントした領域を読み込んだり、ネットワーク越しにしていすればよいでしょう。

$ docker run -d -p 81:8080 -v $HOME/git/swagger-dart-client/:/usr/share/nginx/html/yaml \
    -e API_URL=yaml/petstore.yaml swaggerapi/swagger-ui

http://localhost:81 にアクセスすれば確認できます。

f:id:takamii228:20200506121406p:plain

Swagger CodegenDartのコードを自動生成する

FlutterでAPIアクセスをして画面を描画する時、APIのモデルデータの一式を定義するのは大変です。

PHPJavaScriptであれば単なるArrayのMapで処理するだけなのですが、JavaDartではきっちり型定義が必要なのでList型や入れ子JSONを含む複雑なレスポンスを返すAPIの場合は定義がとても面倒です。

加えて、特にフロントエンドとバックエンドが並行して新規開発するケースでは、やっとの思いでモデルクラスを定義しきれたとしてもAPIの仕様が流動的なため頻繁な変更が入ります。Typoでミスが入り、バックエンドと繋ぐときにIFの不整合が多発する要因にもなります。

そこでSwagger/OpenAPIのyamlで仕様を定義し、クライアントコードを自動生成するという戦略をとることでその手間やリスクを減らすことがことできます。

ツールをインストールする

コードの自動生成ツールをは、Swagger/OpenAPIそれぞれ用意されているのですが今回はOpenAPIの方を利用します。

インストールは公式のガイドに従っていけばよいです。

openapi-generator.tech

npm、homebrew、docker、jar、bashの5種類用意されています。

それぞれが利用しやすいものを選べばよいと思いますが、今回はjarを利用することにします。

API定義のYamlを記述する

まず YamlAPIの仕様の肝となり、プロダクションで利用するコードの自動生成にも利用するものなので、フロントエンドとバックエンドをつなぐ重要な設計成果物であることをお互いに認識しましょう。

これが非常に重要です。

「ファイルサーバのExcelの方が最新です」ということにならないように、Gitのレポジトリで管理され、かつバージョニングされていることが望ましいです。

Yamlの記述についてはSwagger Editorを使えばSyntaxを確認できますが、yamlが巨大になってくるとブラウザが悲鳴を上げてくるのでcliで実行するとよいでしょう。

$ java -jar openapi-generator-cli.jar validate -i petstore.yaml 
Validating spec (petstore.yaml)
No validation issues detected.

Yamlを手動で記述するのはなかなか大変なので、記述しやすい別の設計ドキュメントから自動生成するようにしてもいいかもしれませんね(何とはいいません🙊🙊🙊)。

個人的にはバックエンドのソースコードからYamlを生成してもらえると実装と設計の一意性がより担保できると思ってます。

YamlからDartのコードを自動生成する

Yamlが出来上がったらあとはクライアントコードの自動生成をするだけです。

Flutterはdartで記述するのでdartのクライアントコードを生成するのですが、GeneratorのREADMEを読むとdartdart2とあります。

Flutterが利用するdartのバージョンは2.x系なのでdart2を利用すればいいのねと思ってコマンドを実行したら

$ java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart2 -o build
Can't load config class with name 'dart2'
...
[error] Check the spelling of the generator's name and try again.

となってエラーで失敗しました。まだコマンドラインツール上ではdart2は対応していないようです。

githubを見に行くとdart2用のテンプレートは用意されているようなのでテンプレートを指定する形で利用すればよさそうです。

github.com

テンプレートは-tオプションで指定することができます。

$ java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart -t template/dart2 -o build
[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - No .openapi-generator-ignore file found.
[main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: dart (client)
[main] INFO  o.o.codegen.DefaultGenerator - Generator 'dart' is considered stable.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.DefaultGenerator - Model Pets not generated since it's an alias to array (without property) and `generateAliasAsModel` is set to false (default)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/model/error.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/error_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/Error.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/model/pet.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/pet_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/Pet.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/api/pets_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/pets_api_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/PetsApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.travis.yml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.openapi-generator/VERSION

生成コードをカスタマイズする

OpenAPIを使ったコード生成では何も指定しないとAPIクライアント、ドキュメント、テストコードなどが一式生成されます。

モデルクラスのみあればよいというケースでは、オプションを設定することで自動生成をスキップできます。

$ java -jar openapi-generator-cli.jar -DbrowserClient=false -DapiTests=false -DmodelTests=false \
  generate -i petstore.yaml -g dart -t template/dart2 -o build

詳細は Selective Generation を見るとよいでしょう。

また自動生成されたコードがDartのLintルールに沿ってない場合は、テンプレートのmustacheファイルをいじってルールに沿うように修正するとよいです。もしテンプレートをいじる場合は忘れずにテンプレートファイルをバージョン管理しましょう。

一式の生成の流れがまとまったらレポジトリにまとめたり、API仕様書が変更されたときにFlutterのプロジェクトへどう修正を取り込むのかのパイプラインを考えるとよいでしょう。

Flutterの場合はpubspec.yaml経由でバージョンを指定してimportすることもできるようです。やや古いですが、こちらの記事が参考になります。

まとめ

以上がSwagger/OpenAPIのYamlからコードを生成する流れになります。githubにサンプルもまとめておきました。

頻繁に変更されるAPI定義のモデルクラスの修正は、単純でありつつもミスがゆるされない作業になるため、自動生成の仕組みを上手く利用して効率化するとよいでしょう。

github.com

おまけ

Swaggerを使ったコードやドキュメントの自動生成の話はint128先生のJavaのSpringを使った例も参考になるので見てみてください。

Swaggerのテンプレートを魔改造した話 / Customize Swagger Templates - Speaker Deck

Visual Studio App Centerを使って内部向けに継続的にネイティブアプリを配信する

はじめに

ネイティブアプリの内部テストを円滑にすすめるために、Visual Studio App Centerを使ってアプリを配信する仕組みを触る機会があったので手順をまとめておきます。

Visual Studio App Centerとは

Microsoftが提供しているネイティ・デスクトップアプリ開発向けの開発プラットフォームです。Azure DevOpsとの連携やアプリのデバイス上でのテスト実行、ストア配信などもサポートしており、ビルドやテストの実行に一定時間の無料枠があります。

visualstudio.microsoft.com

今回はビルド済みのAndroidアプリおよびiOSアプリをテスト端末へ配信する手順をまとめます。

1. Visual Studio App Centerの準備をする

アカウントとプロジェクトを作成する

Visual Studio App Centerは以下のID連携を使ってアカウントを作成することができます。

以前はメールアドレスでのアカウント作成もできたようなのですが、今は上記のアカウントからしか利用できないようです。メールアドレスで作成済みのユーザはメールアドレスにてログインできるようです。

今回はGoogleアカウントで作成します。作成するときにOwner Nameを定義することができ、URLに利用されます。

ログインでの認証でIP制限を用いたいケースは、アカウント管理をAzureのIDアカウント管理に任せ、認証画面にFirewallを設定するとよさそうです。

ログインが完了したら、次にアプリのプロジェクトを作成します。

Android用のプロジェクトは右のAdd New appから作成できます。

f:id:takamii228:20200420214929p:plain

今回はBetaとしました。他にも配信中のアプリや内部テストといったラベルがつけられるようです。

iOSも同様に作成します。

f:id:takamii228:20200420215026p:plain

これで前準備は完了です。アカウントのOwner Name、プロジェクトIDをあとで利用するのでそれぞれ控えておきましょう。

f:id:takamii228:20200420215301p:plain

APIトークンを発行する

以後APIにてApp Centerのリソースにアクセスするため、ログインしたユーザのAPIトークンを発行しておきましょう。APIトークンはユーザのAccount Settingsのメニューの中で発行できます。

f:id:takamii228:20200420215755p:plain

2. apkファイル・ipaファイルを作成する

Visual Studio App Center上でもアプリのビルドは実施できますが、ここでは別環境にてビルドしたipaファイル・apkファイルを配布するというシナリオを説明します。

それぞれ個別に作成するのが面倒だったので、Flutterのプロジェクトを作成したときに最初に作られるサンプルアプリをベースにipaファイルとapkファイルを作成しておきます。こういうときはクロスプラットフォームは便利ですね。

apkの署名はデバッグプロファイルで、ipaは後でインストールする端末の情報を含んだprovisioning profileファイルで署名したものを用意します。

Flutterを使ってapk・ipaファイルを作成する手順はこちらを参考にしてください。

takamii.hatenablog.com

3. Visual Studio App Centerへapk・ipaファイルをアップロードする

公式のドキュメントを参考にアップロードに必要な処理を確認します。

docs.microsoft.com

ドキュメントにはcurlコマンドを使った手順がのっているので、今回はこれを参考にシェルスクリプトを組んで行きます。

なおnpmコマンド経由でもできるようですが、Node環境の準備が面倒なので今回は使いません。

docs.microsoft.com

手順を見ればわかりますが、やることは単純でアップロード用のURLを発行して、その発行したURLを使ってファイルをアップロードするだけです。ただしファイルをアップロードするだけだと配信は完了しないため、追加のAPIリクエストを実行して配信のリクエストも自動化してしまいましょう。

APIの仕様書はOpen APIにて公開されています。

openapi.appcenter.ms

なおVisual Studio App Centerで表示されるバージョンはアプリの組み込まれているバージョンで、プロジェクト上の通番はアップロードごとに自動的にRelease IDとしてインクリメントされます。

各Release IDに対してrelease_noteという説明欄を利用できるのですが、これはファイルアップロードのIFでは利用できません。

そのため、リリースとタグ名を紐付けるためにはアップロードが完了したあとに最新のRelease IDを取得し、そのReleaseに対してリリースのタグ名を紐付けるようなPATCHリクエストを投げるとよいでしょう。

最後に配信対象のグループを指定して配信APIを実行すれば配信が開始されます。

以上をまとめると、以下のようなAPIリクエストを順番に実行すればよいです。

  1. ファイルアップロード用のURLを発行する
  2. ファイルをアップロードする
  3. 最新のRelease ID(2で作成されたRelease)を取得する
  4. 3のRelease IDに対応するRelease Noteを更新する
  5. 3のRelease IDの配信を対象のグループに対して開始する

シェルスクリプトでまとめると以下のようになります。

#!/bin/sh

set -x
set -e

# タグ名
RELEASE_TAG_NAME=${1}

# APK ファイルパス
ANDROID_APK_FILE_PATH=${2}

# ipa ファイルパス
IOS_IPA_FILE_PATH=${3}

# Visual Studio App Centerのパラメータ
AZURE_APPCENTER_API_TOKEN='xxxxxxxx'
AZURE_APPCENTER_API_BASE_URL='https://api.appcenter.ms/v0.1/apps'
AZURE_APPCENTER_OWNER_NAME='takamii228'
AZURE_APPCENTER_ANDROID_PROJECT_NAME='AndroidSample'
AZURE_APPCENTER_IOS_PROJECT_NAME='iOSSample'
AZURE_APPCENTER_DISTRIBUTE_GROUP_ID='00000000-0000-0000-0000-000000000000'

# AppCenterにアップロードする一連の処理をまとめた関数
#
# $1: AppCenterのプロジェクト名
# $2: アップロードファイルパス 
# $3: タグ名
#
function upload_file_to_appcenter () {
  UPLOAD_API_BASE_URL=${AZURE_APPCENTER_API_BASE_URL}'/'${AZURE_APPCENTER_OWNER_NAME}'/'${1}

  # アップロード用のURLを発行する
  APP_CENTER_UPLOAD_URL=`curl -v -X POST -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'X-API-Token: '${AZURE_APPCENTER_API_TOKEN} \
    ${UPLOAD_API_BASE_URL}'/release_uploads' | jq -r .upload_url`

  # ファイルをアップロードする
  curl -v -F "ipa=@${2}" ${APP_CENTER_UPLOAD_URL}

  # アップロードしたアプリのリリースIDを取得する
  RELEASE_ID=`curl -v -X GET -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'X-API-Token: '${AZURE_APPCENTER_API_TOKEN} \
  ${UPLOAD_API_BASE_URL}'/releases' | jq 'max_by(.id) | .id'`

  # アップロードの中でRelease Noteは更新できないため、作成されたリリースIDに対してPATCHリクエストを投げる
  curl -v -X PATCH -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'X-API-Token: '${AZURE_APPCENTER_API_TOKEN} \
    -d '{ "release_notes": "'"${3}"'" }' \
    ${UPLOAD_API_BASE_URL}'/releases/'${RELEASE_ID}

  # アップロードしたアプリをCollaboratorsに配信する
  curl -v -X POST -H 'Content-Type: application/json' \
    -H 'Accept: application/json' \
    -H 'X-API-Token: '${AZURE_APPCENTER_API_TOKEN} \
    -d '{ "id": "'"${AZURE_APPCENTER_DISTRIBUTE_GROUP_ID}"'", "mandatory_update": false, "notify_testers": true }' \
    ${UPLOAD_API_BASE_URL}'/releases/'${RELEASE_ID}'/groups'

  echo "App file ${2} was released in Visual Studio App Center in Project: ${1} for TAG: ${3}, Release ID: ${RELEASE_ID}"
}

# apkファイルをアップロードする
upload_file_to_appcenter ${AZURE_APPCENTER_ANDROID_PROJECT_NAME} ${ANDROID_APK_FILE_PATH} ${RELEASE_TAG_NAME}

# ipaファイルをアップロードする
upload_file_to_appcenter ${AZURE_APPCENTER_IOS_PROJECT_NAME} ${IOS_IPA_FILE_PATH} ${RELEASE_TAG_NAME}

上記のシェルスクリプトをtag push契機で実行するようにCDパイプラインを組むとアップロードと配信を自動で実行できるようになります。

$ ./upload-app-to-appcenter.sh 1.0.0-dev app-release.apk Runner.ipa

4. Visual Studio App Centerからアプリをダウンロードする

配信が開始されると対象者にメール通知が飛んできます。 インストールしたい端末にてそのURLを開き、ログインをするとダウロードリンクを取得することができます。

ダウンロード画面のブラウザでの表示はデフォルトではビルド番号・バージョン番号順になっているため、開発中は固定にしている場合はファイルの識別が難しいです。なので3で説明したとおり、release_noteにタグ名を書いておくと識別しやすいでしょう。

Android上でapkファイルをインストールする

Androidの場合はそのままapkファイルをダウンロードすることでアプリを端末にインストールすることができます。

f:id:takamii228:20200420225129p:plainf:id:takamii228:20200420225135p:plainf:id:takamii228:20200420225140p:plainf:id:takamii228:20200420225145p:plain

iOS上でipaファイルをインストールする

iOSの場合はprovisioning profileやiPhoneApple IDのアカウントの設定にもよりますが、appcenterの構成プロファイルのインストールを端末上で求められるケースがあるようです。

f:id:takamii228:20200420224446p:plainf:id:takamii228:20200420224450p:plain f:id:takamii228:20200420224457p:plainf:id:takamii228:20200420224502p:plain

まとめ

以上がVisual Studio App Center経由でapk/ipaファイルを配信する手順になります。

ipaファイル・apkファイルを作成するCDパイプラインとシームレスに繋げば、tag push契機でテスト用アプリを配信することができるようになりますね。

以前紹介したFlutterのCDパイプラインと合わせてみると以下のようなイメージになります。便利〜!

f:id:takamii228:20200427110819p:plain

内部で手軽にアプリを配信できるような仕組みを用意しておくことで、モバイルアプリでもサーバサイドと同等レベルのよりEnd-to-Endの継続的デリバリーが実現でき、QAやUI確認等の開発のフィードバックライフサイクルをより短縮することができるでしょう。

余談

AppCenter便利やわ〜と思ってた矢先に、配信済みの資材が一斉に消えるという場面に遭遇しました。

え、やっぱ課金必要だったの!?とか焦ってたら公式から障害の連絡が出てました。

何があったかは書いてないですが、Distributeの部分に障害があったとのこと。焦りました。

App Center Status - Distribute is Experiencing Issues

サービスのステータスはこちらで確認できるようです。「あれ?」と思ったときはまずこちらを見てみるようにしましょう。

status.appcenter.ms

Flutterを使ったAndroid・iOSアプリ開発のCDパイプラインを構築する #flutter

はじめに

前回はCIパイプラインについて前半・後半に分けて説明を行いました。

takamii.hatenablog.com

takamii.hatenablog.com

今回はCDパイプラインについて説明します。

CDとは継続的デリバリー(Continuouse Delivery)の略で、アプリケーションを継続的に短いサイクルでリリース可能な状態にするプラクティスのことを指します。

Android / iOSアプリにおけるCDパイプラインとは、ソースコードからアプリを端末にインストールしたりストア申請可能な状態であるapkファイル・ipaファイルをいつでも作成できる状態にする、ということになります。

今回の検証に利用したFlutter環境は以下の通りです。

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[] Flutter (Channel unknown, v1.12.13+hotfix.9, on Mac OS X 10.15.4 19E266, locale en-JP)
[] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[] Xcode - develop for iOS and macOS (Xcode 11.4.1)
[] Android Studio (version 3.6)

また今回はapk/ipaファイル作成のみについて触れるのですが、実際の開発現場では検証環境や内部テスト向けのアプリと実際の配信するアプリを環境差分で分けて管理する必要があると思います。

Flutterの場合はflavorを独自に定義することで、それらについても対応することができるのですが今回はそこの説明はスコープ外とします。

軽く触れると、Androidは通常のAndroidと同様のやり方でできます。iOSについては命名規則に従ってSchemeを定義すればよいです。 こちらが参考になります。

medium.com

Flutterを使ったAndroidアプリでapkファイルを作成する

Flutterで構築したアプリケーションからapkファイルを作成する方法は基本的には公式のガイドに従えばよいです。

flutter.dev

Android固有の設定

FlutterのAndroid側のプロジェクトにはandroid pluginを内包しているため、基本的には通常のAndroidと同じビルド設定で問題ありません。 

公式のガイド通りに以下をbuild.gradleに設定していけばよいでしょう。

  • apkの署名設定
  • R8によるアプリ圧縮設定
  • R8(proguard)による難読化設定
  • バージョン番号とバージョンコードの設定

Flutterにはデフォルトでdebugprofilereleaseの3種類のbuildTypeが用意されていますが、apkを作るときにはreleaseのところに設定を記述しましょう。

ビルドスクリプトを構築する

公式ガイド通りにflutter build apk --releaseコマンドを実行すればよいです。

引数でtarget-platformやビルド番号・バージョン番号が指定できるので、32bitと64bitを分けてビルドする場合は指定するようにしましょう。

FlutterはAppBundleにも対応しているようなのでそれを利用しても良いかもしれません。

今回はバージョン番号を引数に受け取って32bit/64bit別々にビルドするシェルスクリプトを作ってみました。

ストア申請のときに32bitと64bitを分けて申請する場合は32bitのバージョン番号より64bitのバージョン番号を大きくしておく必要があるので、シェルの中で+1するようにしました。

developer.android.com

build-android-apk.sh

#!/bin/sh
set -x
set -e

# TARGET_APP_VERSION
TARGET_APP_VERSION=${1}

# TARGET_VERSION_CODE
TARGET_VERSION_CODE_ARM=${2}
TARGET_VERSION_CODE_ARM64=$(expr ${2} + 1)

# Artifacts Path
OUTPUT_APK_PATH="./artifacts/android"

# 依存関係を解決
flutter pub get

# cleanを実行
flutter clean

# arm用のapkを作成
flutter build apk --release --target-platform=android-arm \
  --build-name=${TARGET_APP_VERSION} --build-number=${TARGET_VERSION_CODE_ARM}

# 成果物をコピー
cp build/app/outputs/apk/release/app-release.apk \
  ${OUTPUT_APK_PATH}/app-release-arm-${TARGET_APP_VERSION}.${TARGET_VERSION_CODE_ARM}.apk

# cleanを実行
flutter clean

# arm64用のapkを作成
flutter build apk --release --target-platform=android-arm64 \
  --build-name=${TARGET_APP_VERSION} --build-number=${TARGET_VERSION_CODE_ARM64}

# 成果物をコピー
cp build/app/outputs/apk/release/app-release.apk \
  ${OUTPUT_APK_PATH}/app-release-arm64-${TARGET_APP_VERSION}.${TARGET_VERSION_CODE_ARM}.apk

実行ログは以下のようになります。

$ ./build-android-apk.sh 1.0.0 100
+ set -e
+ TARGET_APP_VERSION=1.0.0
+ TARGET_VERSION_CODE_ARM=100
++ expr 100 + 1
+ TARGET_VERSION_CODE_ARM64=101
+ OUTPUT_APK_PATH=./artifacts/android
+ flutter pub get
Running "flutter pub get" in appcenter_sample...                    0.3s
+ flutter clean
Cleaning Xcode workspace...                                         3.0s
Deleting build...                                                  105ms
Deleting .dart_tool...                                               1ms
+ flutter build apk --release --target-platform=android-arm --build-name=1.0.0 --build-number=100
Removed unused resources: Binary resource data reduced from 44KB to 35KB: Removed 20%
Running Gradle task 'assembleRelease'...                                
Running Gradle task 'assembleRelease'... Done                      39.4s
✓ Built build/app/outputs/apk/release/app-release.apk (5.1MB).
+ cp build/app/outputs/apk/release/app-release.apk ./artifacts/android/app-release-arm-1.0.0.100.apk
+ flutter clean
Cleaning Xcode workspace...                                         2.0s
Deleting build...                                                   82ms
Deleting .dart_tool...                                               2ms
+ flutter build apk --release --target-platform=android-arm64 --build-name=1.0.0 --build-number=101
Removed unused resources: Binary resource data reduced from 44KB to 35KB: Removed 20%
Running Gradle task 'assembleRelease'...                                
Running Gradle task 'assembleRelease'... Done                      35.3s
✓ Built build/app/outputs/apk/release/app-release.apk (5.4MB).
+ cp build/app/outputs/apk/release/app-release.apk ./artifacts/android/app-release-arm64-1.0.0.101.apk

Flutterを使ったiOSアプリでipaファイルを作成する

Android同様、iOSでもFlutterの公式ドキュメント通りに実行すればよいです。

flutter.dev

iOS固有の設定

通常のiOSアプリ開発と同様、ipaファイル作成に必要な証明書とprovisioning profileを準備してビルドを実行するマシンにインストールして おきます。

Android同様、FlutterにはデフォルトでDebugProfileReleaseの3種類のSchemeが定義されていて、ipa作成時にはReleaseのスキームを利用します。Androidと異なって頭文字が大文字な点に注意が必要です。

追加でビルド時に実行したいものがあればReleaseのスキームの設定の中に追記しておくとよいでしょう。

ビルドスクリプトを構築する

公式ガイドを読むと以下を順番に実行せよと書いてあります。

  1. flutter build ios --releaseを実行する
  2. XCode上でバージョン番号とビルド番号を設定し、Generic iOS DevicesでArchiveビルドを実行する
  3. Archiveファイルをビルドしてipaファイルを作成する

要はflutterコマンドを実行した後にxcodebuildコマンドを実行すればよいのです。

Archiveファイルからipaファイルを作成するときにExportOptions.plistファイルが必要なので、環境に応じて準備しておきましょう。

iOSもバージョン番号は外から注入できるようにしてスクリプトを組みましょう。

build-ios-ipa.sh

#!/bin/sh
set -x
set -e

# TARGET_APP_VERSION
TARGET_APP_VERSION=${1}

# TARGET_BUILD_NO
TARGET_BUILD_NO=${2}

# Artifacts Path
OUTPUT_APK_PATH="./artifacts/ios"

# 依存関係を解決
flutter pub get

# cleanを実行
flutter clean

# flutter buildを実行
flutter build ios --release \
  --build-name=${TARGET_APP_VERSION} --build-number=${TARGET_BUILD_NO}

# build archive file
cd ios

EXPORT_OPTION_PLIST="ExportOptions.plist"

xcodebuild -workspace Runner.xcworkspace -scheme Runner -sdk iphoneos \
  -configuration Release archive -archivePath ../build/Runner.xcarchive

# build ipa file
xcodebuild -allowProvisioningUpdates -exportArchive \
  -archivePath ../build/Runner.xcarchive \
  -exportOptionsPlist ${EXPORT_OPTION_PLIST} -exportPath ../build/ios-release

cd ..

# 成果物をコピー
cp ./build/ios-release/Runner.ipa ${OUTPUT_APK_PATH}/app-release-${TARGET_APP_VERSION}.${TARGET_BUILD_NO}.ipa

実行ログは途中は省略しますが以下のようになります。

$ ./build-ios-ipa.sh 1.0.0 100
+ set -e
+ TARGET_APP_VERSION=1.0.0
+ TARGET_BUILD_NO=100
+ OUTPUT_APK_PATH=./artifacts/ios
+ flutter clean
Cleaning Xcode workspace...                                         2.0s
Deleting build...                                                   34ms
Deleting .dart_tool...                                               0ms
+ flutter build ios --release --build-name=1.0.0 --build-number=100
Building com.takamiii.appcentersample for device (ios-release)...
Automatically signing iOS for device deployment using specified development team in Xcode project: xxxxxxxx
Running Xcode build...                                                  
                                                   
 ├─Building Dart code...                                    13.9s
 ├─Generating dSYM file...                                   0.1s
 ├─Stripping debug symbols...                                0.0s
 ├─Assembling Flutter resources...                           0.7s
 └─Compiling, linking and signing...                         9.0s
Xcode build done.                                           26.1s
Built /Users/takami228/Documents/flutter-dev/flutter_sample/build/ios/iphoneos/Runner.app.
+ cd ios
+ EXPORT_OPTION_PLIST=ExportOptions.plist
+ xcodebuild -workspace Runner.xcworkspace -scheme Runner -sdk iphoneos -configuration Release archive -archivePath ../build/Runner.xcarchive
...
** ARCHIVE SUCCEEDED **

+ xcodebuild -allowProvisioningUpdates -exportArchive -archivePath ../build/Runner.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath ../build/ios-release
2020-04-26 17:29:59.502 xcodebuild[34691:937911] [MT] IDEDistribution: -[IDEDistributionLogging _createLoggingBundleAtPath:]: Created bundle at path '/var/folders/6n/llxh8m296896rymtgjfbkmxw0000gn/T/Runner_2020-04-26_17-29-59.501.xcdistributionlogs'.
Exported Runner to: /Users/takami228/Documents/flutter-dev/flutter_sample/build/ios-release
** EXPORT SUCCEEDED **

+ cd ..
+ cp ./build/ios-release/Runner.ipa ./artifacts/ios/app-release-1.0.0.100.ipa

クラッシュレポートやFirebase Crashlyticsでクラッシュを解析するためにビルド時のdSYMファイルが必要になるケースがあるので、忘れないようにここでアップロードしたり退避させるのがよいでしょう。

CD環境と統合する

あとはbuild-android-apk.sh および build-ios-ipa.sh を署名情報や証明書がインストールされたマシンやCI環境でtagプッシュ契機で実行するようにすればCDパイプラインの完成です。

下の図はGitLab CIでの実行の流れを表した図になります。

f:id:takamii228:20200426175707p:plain

GitLab CIでは以下のように.gitlab-ci.ymlを定義すればよいでしょう。

...
release:
  stagie: build
  tags:
    - release-ci-runner
  only:
    - tags
  script:
    - ./build-android-apk.sh ${TARGET_APP_VERSION} ${TARGET_VERSION_CODE} 
    - ./build-ios-ipa.sh ${TARGET_APP_VERSION} ${TARGET_BUILD_NO} 

まとめ

CI編と合わせてapk、ipaの作成を自動化するCDパイプラインを構築する例を説明しました。

CDパイプラインを自動化しておくことでリリース作業のハードルが下がり、リリース作業の属人化を防ぎ、結果としてリリース頻度を増やすことができます。

リリース頻度を増やすことはサービスの競争力を強化する大きなポイントとなるので、ぜひ参考にしてみてください。

またCIパイプラインと同様に、CDパイプラインも一度作って終わりではなく、アプリケーションを提供し続ける限り同様にメンテナンスが必要です。

Flutterはまだまだ変化が激しい技術領域なので、これから破壊的な変更が発生する可能性は0ではないので、前のめりで追従していく気持ちで頑張ってついていきましょう。