Infinito Nirone 7

白羽の矢を刺すスタイル

Android アプリのモジュールで kotest を使いユニットテストを記述する

以前は Kotlintest と呼ばれたテストフレームワークが Kotest と名前を変えて開発が進められています。

github.com

このテストフレームワークは非常に多様なテストの記述方法をサポートしており、テストケースのスタイルからアサーションメソッド、パラメトライズドテストのサポートまでテストに必要な幅広い関心をカバーしています。 マルチプラットフォームでの動作をサポートしたテストフレームワークとなっていて、自分で拡張を導入するための構造も持っています。公式にも様々な拡張が提供されていて、Robolectric と一緒にテストを動作させるための拡張も用意されています。 ただし2020年10月現在 Robolectric の拡張は experimental であり、うまく動作しないパターンが多々あります。この記事では、次に示す各ライブラリのバージョンを組み合わせて kotest を Android アプリプロジェクトに導入する方法を紹介します。

  • kotest: 4.2.5
  • kotest-robolectric-extension: 4.0.1
  • Robolectric: 4.4

依存関係

build.gradle に下記の記述を加え、kotest および Robolectric を導入します。

android{
  testOptions {
    unitTests.all {
      useJUnitPlatform()
    }
  }
}

dependencies {
  // kotest
  testImplementation 'io.kotest:kotest-runner-junit5:4.2.5'
  testImplementation 'io.kotest:kotest-assertions-core:4.2.5'
  testImplementation 'io.kotest:kotest-property:4.2.5'
  testImplementation 'io.kotest:kotest-extensions-robolectric-jvm:4.0.1'

  // robolectric
  testImplementation 'org.robolectric:robolectric:4.4'
}

必要に応じて、テストの source directory 配下に resources ディレクトリを掘って、robolectric.properties ファイルを作ります。

sdk=28
application=your.app.pkg.YourApp

kotest-robolectric-extension による Robolectric の有効化

テストの source directory の好きなパッケージに ProjectConfig オブジェクトを作ります。

ProjectConfig オブジェクトは様々な extension を追加したり、テストの並列数を制御したりするためのオブジェクトです。 Robolectric を kotest で動作できるようにするには、extensions で RobolectricExtension を含むリストを返します。

また、Robolectric を使う場合の制約として、IsolationMode を InstancePerLeaf にしておく必要があります。IsolationMode はテストケースのインスタンスをどの単位でもつかを制御するパラメータで、InstancePerLeaf がもっとも細かい単位でインスタンスを作ります。現状の kotest robolectric extension による Robolectric の初期化処理では、モジュール内に複数のテストを記述したファイルがある場合、Looper の初期化処理が複数回呼ばれてしまい例外が投げられることがあります。IDE では個別にテストを実行するため気付きにくいですが、CLI で一気にテストを実行するとこのエラーに遭遇します。InstancePerLeaf ではテストケースのインスタンスごとに Robolectric の設定も異なる扱いをうけるためか、この初期化処理の重複による失敗を回避できます。

object ProjectConfig : AbstractProjectConfig() {

  override val isolationMode: IsolationMode = InstancePerLeaf

  override fun extensions(): List<Extension> = listOf(RobolectricExtension())
}

ちなみに、筆者が試した限り Robolectric を有効化したモジュールで ProjectConfig の parallelism を 2 以上にした場合も Robolectric の内部状態が不正になりテストが実行できなくなりました。

テストの記述

kotest-robolectric-extension で Robolectric を有効化したモジュールでは、自分が確認した限り次のテストスタイルが利用可能です。これら以外を使用すると Robolectric の初期化処理が複数回呼ばれてテストが失敗します(とくに BehaviorSpecDescribeSpecFeatureSpec)。

  • StringSpec
  • WordSpec
  • FunSpec

Robolectric を利用するテストには @RobolectricTest アノテーションを付けます。 が、現状の kotest-robolectric-extension では同じモジュール内で Robolectric を利用しなくてもよいテストを記述している場合でも、@RobolectricTest アノテーションを付けておかないとテストが実行できない(テストケースがひとつも実行されず正常終了する)ため、Robolectric が不要なテストにも @RobolectricTest アノテーションを付与することになります。

@RobolectricTest
class SomeTest : StringSpec() {
  init {
    "something" {
      // Given: Values
      val a = 0
      val b = 1

      // When: Subtract
      val subtracted = a - b

      // Then: subtracted value should be -1
      subtracted.shouldBe(-1)
    }
  }
}

マルチモジュールプロジェクトでの kotest の設定

先に紹介した ProjectConfig はモジュールごとに定義します。マルチモジュールな状況では kotest を使うすべてのモジュールで ProjectConfig オブジェクトを作る必要があります。少々面倒ではありますが、これにより、Robolectric を使うモジュールとそうでないモジュールを切り分けることができるため、Robolectric が必要なモジュールでは StringSpec を用い、そうでないモジュールでは BehaviorSpec を用いるなどのスタイルの使い分けがしやすくなります。

チームで育てる Android アプリ設計プロジェクト、始まります

Peaks さんで「チームで育てる Android アプリ設計」と題した執筆プロジェクトに @kgmyshinさんと取り組むことになりました。この記事はこのプロジェクトが発足するまでの経緯や、執筆する本に込めた思いを伝えられればと言うことで書いています。

チームとアーキテクチャを両輪で語る書籍

これまでの様々なアーキテクチャのあるべき論から少し離れてみて、どうやったらうまくチームでアーキテクチャを扱えるようになるかを、釘宮さんと自分のそれぞれで得た経験をベースに話していく書籍が「チームで育てる Android アプリ設計」です。 アーキテクチャは一度整えればそれで終わるわけではなく、プロダクトの成長やチームの成長とともに少しずつ形を変えていくものであるという考えのもとで、新規開発チームでの事例と、走り始めたあとの規模の大きなチームでの事例を紹介していく予定です。

チームで育てる Android アプリ設計

この書籍のタイトルに込めた思いとして、チーム全員の目線を合わせてアーキテクチャを考えていきたいという思いがあります。

当初よりチームとアーキテクチャを両輪でうまくまわしていくための書籍を書きたいという思いからこのプロジェクトが始まりました。そこから、プロダクトが成長していくごとにアーキテクチャに求められるものも変化し、その時々に応じて少しずつアーキテクチャも微妙に形を変えていくはずだよね、という議論があり、さらにチームそのものもその変化に合わせて成長をしていくといいよね、というところから、このタイトルへと落ち着いていきました。

本当はチームがアーキテクチャを育てるだけでなく、アーキテクチャの変化がチームを育てることもあり、そこからさらにチームがアーキテクチャを進化させていくようなループが作れると理想で、そのことをうまくタイトルで表現できると良かったのですが、ひとまずはチーム一丸となってアーキテクチャを形づくり継続的に整えていくことを伝えるために「チームで育てる Android アプリ設計」という名前になりました。 そしてこのチームが一丸となってアーキテクチャの形を作り続けていくという姿が「都市計画」のようなイメージを彷彿とさせたことから、表紙のイメージはまるでシムシティで都市を発展させていったときの様子を写したようなイメージにしています。

さいごに

この書籍では、特定のパターンのアーキテクチャがよいという話ではなく、チームやプロダクトの状況からうまくアーキテクチャを整え、ともに成長していくことにフォーカスした内容に仕上げていく予定です。 個人的にはテストしたいところがテストしやすければアーキテクチャはなんでもよいと思っているのですが、数多あるパターンの中からうまく時流や要求にあった形にで落とし込めそうなものを模索してきた経緯を文章にできれば、きっと他のいろいろなプロジェクト・チームでも役に立つだろうと思っています。 ぜひ、応援をよろしくおねがいします。

クラウドファンディングサイト

peaks.cc

よろしくおねがいします!

Podcast を Apple Podcast や Google Podcast に登録する

Podcast を配信するにあたって、Apple Podcast や Google Podcast に登録しておくと、iPhone や Android のアプリから気軽に Podcast を聴けるようになります。各プラットフォーム向けに配信の設定をしておくと、アプリで Podcast の未読管理や前回からの視聴の再開、Chrome Cast での視聴などができて便利です。

この記事では Apple Podcast と Google Podcast で Podcast を配信をする手順をまとめておきます。前提として、Podcast の RSS フィードを何らかの方法で生成しておいてください(r7kamura/yattecastで静的に生成するなど)。RSS フィードには必須のタグと推奨のタグがそれぞれあり、Google のサポートページ を参考にしてください。必須のタグが欠けていたり、RSS フィードが誰でもアクセスできる場所にないとうまく登録できないことがあります。

Apple Podcast

Apple Podcast はちょっと前まで iTunes に統合されていたもので、今は Podcast アプリとして独立しています。配信するには iTunes Connect アカウントが必要です。

iTunes Connect: https://itunesconnect.apple.com/

ログイン後、Podcasts Connect を選択すると RSS フィードを登録する画面が出てくるので、配信したい Podcast の RSS フィードの URL を入力し、必要なデータを入れ登録を申請します。 1日ほど審査に時間がかかりますが、審査が通るとすぐに審査完了のお知らせが iTunes Connect アカウントのメールアドレスに届きます。お知らせには Apple Podcast で登録した Podcast 専用の URL も記述してあるので、それを自分の Podcast のウェブサイトにリンクとして貼って使えます。

Google Podcast

Google Podcast は Google Play Music に統合されていたもので、今は Google Podcast への移行が推奨されています。ちょっと前までは Google Play Music 向けに Podcast の登録ができましたが、今は Google Podcast Manager を使う必要があります。

Google Podcast Manager: https://podcastsmanager.google.com/

Google Podcast では、Podcast の RSS フィード URL を登録後、更にいくつかのステップを踏まなければなりません。RSS フィードを登録すると、RSS フィードに含まれるポッドキャストの Author のメールアドレス宛に、RSS フィードが Google Podcast に登録されたことを通知するメールが届きます。

ここで Podcast の登録が意図したものかどうかの確認をします。メールには確認コードが含まれるため、このコードを入力すると Google Podcast に Podcast が登録されます。ただし登録が終わってもすぐには Google Podcast には反映されません。RSS フィードがインデックスに登録されるまでに2〜3日かかるので、気長に待ちましょう。

Spotify for Podcasters

Spotify で Podcast を配信できます。

Spotify for Podcasters: https://podcasters.spotify.com/

アカウント登録後、RSS フィードを登録すると、RSS フィードのオーナー情報などに記載されているメールアドレスに確認コードが送られます。そのコードを入力したら、諸々配信者の情報を入力したり、ポッドキャストのカテゴリやサブカテゴリを選んだりしたら登録が完了します。 最後のステップで Spotify での配信 URL が表示されるので、時間を開けてアクセスすると聴けるようになっています。

統計データ

Apple Podcast も Google Podcast も、Podcast がどれくらい聴かれているかの統計データが閲覧できます。それぞれ iTunes Connect や Google Podcast Manager で閲覧できるようになっているので、配信側としても簡単に統計データを取れて便利です。

在宅勤務で手元にない端末でのアプリのデバッグをするために準備しておくこと

コロナ禍で在宅勤務が一気に広まり、自宅でアプリの開発やリリースをすることが多くなりました。この状況のなかで、アプリの挙動に問題があるなどで動作確認をしようと思うとき、その問題に対処するためのデータを集めたり、問題が発生したときの状況を確認するための手立てを持っておく必要があります。この記事では、手元にない端末で起きる問題のトラブルシューティングをしやすくするために準備しておくとよいことを書き残しておこうと思います。

端末のログを収集する

ある端末でアプリがクラッシュしたときに、そのスタックトレースを記録しておくことはとても重要ですが、そのクラッシュにいたるまでに何があったかをログとして収集しておくことで、クラッシュが起きた状況を把握しやすくなります。

Firebase CrashlyticsDeployGate など、端末からログを収集して Web コンソール上で閲覧できるようにしておくと、なにか問題が起きたときにそれまでのログをいつでもリモートで確認できます。Timber などと組み合わせて、各ログレベルのログを収集できるようにしておくとよいです。

Firebase Crashlytics の場合:

class CrashlyticsTree : Timber.Tree() {
  override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
    when (priority) {
      Log.VERBOSE -> {
        Crashlytics.log(priority, tag, message)
      }
      Log.DEBUG -> {
        Crashlytics.log(priority, tag, message)
      }
      Log.INFO -> {
        Crashlytics.log(priority, tag, message)
      }
      Log.WARN -> {
        Crashlytics.log(priority, tag, message)
      }
      Log.ERROR -> {
        Crashlytics.log(priority, tag, message)
        t?.let {
          Crashlytics.logException(it)
        }
      }
      else -> {
        Crashlytics.log(priority, tag, message)
        t?.let {
          Crashlytics.logException(it)
        }
      }
    }
  }
}

DeployGate の場合:

class DeployGateTree : Timber.Tree() {
  override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
    if (!DeployGate.isInitialized())
      return

    when (priority) {
      Log.VERBOSE -> {
        DeployGate.logVerbose(message)
      }
      Log.DEBUG -> {
        DeployGate.logDebug(message)
      }
      Log.INFO -> {
        DeployGate.logInfo(message)
      }
      Log.WARN -> {
        DeployGate.logWarn("$message $t")
      }
      Log.ERROR -> {
        DeployGate.logError("$message $t")
      }
      else -> {
        DeployGate.logError("wtf: $message $t")
      }
    }
  }
}

カスタムキーの設定

Firebase Crashlytics はロギング以外に Key-Value ペアで表現するカスタムキーを設定できます。これをつかうと、端末のログでは収集しきれないデータを収集できます。主に永続化されている設定値(アプリ内での設定として設けている、ユーザが好きに選べる設定の値)や、バージョン情報以外のビルド時のデータ(BuildConfig に設定している値)をもたせておくと、特定の設定値のユーザのみクラッシュするなど状況を把握するのに役立ちます。

カスタムキーを設定した状態でアプリがクラッシュすると、クラッシュレポートにすべてのカスタムキーが紐付いて保存されます。

Crashlytics.crashlytics().setCustomValue(100, "preference_something_duration_int")

Crashlytics のクラッシュレポートを Slack に通知する

新規クラッシュを検知した場合や、短時間に高頻度でおなじクラッシュが発生した場合など、いくつかの条件で Crashlytics から Slack に通知する設定があります。 リリース後のプロダクションビルドではもちろん、QA 中などリリース前のビルドでも有効にしておくと、テストしている人からのバグレポートの補助的な役割として利用できます。

リモートでログを確認する

リモートで今動いているアプリのログを確認する方法もあります。DeployGate や BUGFENDERShipbook などがこの機能を持っています。この方法であれば、クラッシュ以外にもログを集められます。

リモートで端末を操作する

リモートで端末を操作するための手段もあります。 AWS Device FarmKobiton など幅広いモバイル端末を用意したサービスから、オンプレミスな環境にOpen STFを立ち上げでおくもの、あるいは Samsung Remote Test Lab のようにマニュファクチャラーが用意しているサービスを利用するなどで、ブラウザや専用のアプリケーションからリモートにある端末でアプリを操作できます。

バグ報告を円滑にするためのデバッグ情報を表示する

クラッシュレポートやログ以外にも、端末に関すデータやアプリのビルドに関するデータもデバッグに役立ちます。リリース前のアプリでだれでも簡単にこれらのデータを見られるようデバッグ用の表示を作っておくと、QA やドッグフーディングなど社内でのテスト中に見つかったクラッシュや不具合の報告により多くのデータを含められるようになります。 簡単な例としては JakeWharton/u2020 にあるようなデバッグ用の UI を用意するものですが、Drawer でなくとも、設定画面にデバッグ情報を見る画面を開くメニューを置いておくでもよいです。

このデバッグ情報として入れておくとよいものを次にあげます。

  • git のコミットハッシュ
  • git のブランチ名
  • デバイスのモデル名やマニュファクチャラー名
  • API Level
  • ディスプレイのスペック (解像度や dpi など)

See also

speakerdeck.com

SnapCamera で使える、背景を合成するレンズ3分クッキング

SnapCamera で使えるエフェクト(レンズ)は Lens Studio - Lens Studio by Snap Inc. というツールで自作できます。 カメラの映像から人だけをくり抜いて背景をはめるレンズから、3D モデルを配置したり、簡単な物理演算を組み合わせて 3D モデルを動かしたりするレンズまで、いろいろなレンズが作れます。

というわけで背景をはめるレンズを作るところをやってみたので、Lens Studio の使い方をすこし解説してみようと思います。

Lens Studio でプロジェクトを作成する

Lens Studio を起動すると、おすすめプロジェクトテンプレートや自分の作ったプロジェクトなどが一覧できるホーム画面が表示されます。

f:id:KeithYokoma:20200413223152p:plain

背景をはめるレンズは Segmentation というテンプレートから簡単に作れるので、これを使います。

背景を設定する

プロジェクトを読み込むと、次のような画面が出てきます。 左側はプロジェクトで使う画像や 3D モデルなどを管理するペイン(Resources, Objects)、真ん中には背景や3Dモデルとカメラ、光源の位置関係を編集するペインとレンズで使う効果のパラメータを調整するペイン (Inspector) があり、右側には作っているレンズのプレビュー (Preview) が表示されているはずです。

f:id:KeithYokoma:20200413223551p:plain

まず、設定したい背景を Resources ペインにドラッグ・アンド・ドロップで読み込ませます。

f:id:KeithYokoma:20200413224807p:plain

次に、読み込んだ背景をはめ込むために Objects ペインから SegmentationController [EDIT ME] を選び、Inspector でパラメータを変更します。

デフォルトでは半透明のピンク色にハートの画像が斜めに移動する背景が設定されています。 このピンク色やハートの画像は InspectorScript 部分で設定しています。

f:id:KeithYokoma:20200413224932p:plain

今回は画像をはめたいだけなので、Use Background Color のチェックを外します。つぎに、 Image Texture のファイル名をクリックし、画像選択ダイアログから先ほど読み込んだ画像を指定します。

f:id:KeithYokoma:20200413225058p:plain

すると斜めに動いていたハートが指定した画像に変わります。あとは Tiled のチェックボックスを外せば画像が固定されます。

f:id:KeithYokoma:20200413225213p:plain

以上で背景を自分の好きな画像に設定できました。ここで左上の Publish Lens でプロジェクトをアップロードし、Live になるのを待つと SnapCamera で選択できるようになります (検索にヒットするには時間がかかるようなので、Live になった Lens を Share するボタンで URL をコピーしておくと、すぐに検索窓から直接自分の Lens を選べます)。

デスクトップ画面でのプレビューを見る

プレビューでデスクトップ画面を再現するときは、プレビューの下の Device Simulation から Desktop を選びます。

f:id:KeithYokoma:20200413225349p:plain

ポートレートではなく、顔だけくりぬく

電波少年のようにする場合、顔だけをくりぬいて背景を合成したいですよね。

Lens Studio では、ポートレートのほか、顔だけ、首から下、髪だけをくり抜くオプションを設定できます。 この設定を変えて電波少年を再現するには、Resources ペインにある Segmentation Texture [EDIT ME] を選び、Inspector に表示される TextureTypePortrait Face に変えます。

f:id:KeithYokoma:20200413225912p:plain

このままだとくりぬいた顔の部分に背景が合成されてしまうため、すぐ下の Invert Mask にチェックを入れます。すると顔以外の部分に背景が当てはめられます。

f:id:KeithYokoma:20200413230031p:plain

これでいつでも電波少年になります。

我が家のリモートワーク環境

昨今の情勢を受けてリモートワークが始まり1ヶ月ほど経ちました。

キャンプチェアとローテーブルでの作業環境

以前は自宅にもクルーズアンドアトラスを置いて会社と同じデスク環境を持っていましたが、引っ越しを期に手放してしまったために今はHelinox のキャンプチェアローテーブルを使っています。最初のオフィスデスク・チェアがクルーズアンドアトラスで3年ほど仕事をし背もたれを後ろに倒して座るやり方になれきっているので、キャンプチェアでも後傾姿勢で座っています。

リモートワーク開始当初は会社支給の MacBook Pro を使って作業しており、ラップトップ特有の事情でどうしても前傾姿勢での作業になってしまうため、だんだんと辛くなってきました。最近は会社のサポートとして会社にある iMac を自宅まで届けてくれる施策をバックオフィスチームが実行してくれたおかげで、ローテーブルに iMac と MacBook Pro を置いて作業をしていますが、依然として椅子の座り方がどうしても後傾姿勢にできない問題がありました。

iMac が手元にあるいま、キャンプチェアで後傾姿勢になるためには、キーボードやトラックパッドをひざ上に載せられる環境が必要です。最初はキャンプグッズのアルミローテーブルをひざ上に載せ、その上にキーボードやトラックパッドを置いていましたが、若干スペースが足りないのと、キーボードやトラックパッドが滑り落ちてくる問題があったため、サンワダイレクトが出しているひざ上テーブルを購入することにしました(知人が同じようなものを使っていたのを思い出してすぐ買いました)。

ネットワーク

ずっと AirMac Extreme を使い続けてきましたが、随分前に新型のリリースが止まったこと、WiFi の規格も古く、IPoE もできないということで、Fast.com で計測すると 39Mbps しか出ない状況でした。そろそろ日頃つかうものとしては厳しくなってきたため、これを期にAterm WX6000HPを購入し置き換え、無事に WiFi で 410Mbps、有線で 760Mbps でる環境になりました。ハングアウトなどの映像もクリアに映るようになったそうです。

その他

一人ぐらしなのもありリモートワークをすると完全に孤独な作業になります。気にしなければどうということはないのですが、そうは言っても長期間誰の声も聞こえない環境で仕事をするのは寂しいもので、いつもいろいろな工夫をして寂しさを紛らわせています。

さぎょいぷ

ハングアウトなどで擬似的におなじ作業空間をつくり、ぼやきながら作業するチャットルーム的なものです。

Chrome Cast

Netflix、dアニメ、JSports などオンデマンド配信の映像をひたすら Chrome Cast でテレビに映しています。特に JSports はサイクルロードレースのプランを契約しており、最短1.5時間・最長7時間のレース配信は作業用 BGM にとても便利です。

運動

家からでなくなるので、普段の通勤での徒歩すらもしなくなるため、運動を全くしなくなっています。自分は自転車が趣味で100km以上のロングライドが好みですが、休日のサイクリングも難しい状況なので、何かしら代替手段が必要です。幸い、ローラー台 があり Zwift というバーチャルサイクリングの環境もあるので、時間を見つけてローラー台で自転車をこいでいます。

また今月頭にリングフィットアドベンチャーが購入できたので、リングコンが届いたらこれも一緒にできるようになりますね。

持続的なアプリ開発のための DX を支える技術 #DroidKaigi 2020

この記事は、DroidKaigi 2020 で発表予定だったセッション「持続的なアプリ開発のための DX を支える技術」を解説するための記事です。

セッション概要

Android の歴史はすでに 10 年を超え、数多のアプリケーションがストアで公開されています。これらのアプリケーションの中には、何年も継続的にバージョンアップを重ねているものもたくさんあります。

このセッションでは、このような持続的なアプリケーション開発・リリースをうまく回す秘訣として DX という言葉をとらえ、アーキテクチャやテストのほか、日々の開発に関わるワークフローをメンテナンスするための考え方や手立てとして、モバイル CI や Android 向け各種ツールキットの導入と効率化、Gradle をベースにした独自タスク開発の方法などを紹介します。

資料

speakerdeck.com

一部実装の詳細を資料に委ねています。適宜資料と合わせてご覧ください(開設の流れは資料に合わせています)。

解説

このセッションで目指すもの

このセッションでは、エンジニアリングチームとしてよい DX を獲得するための指針として、開発着手前からリリース後の運用に至るまでの間にどんなことに注目するとよいかを示し、モバイルアプリ開発のプロセスを持続的に素早く回していくための考え方や実践例を解説します。

良い DX を支える技術

ここでいう DX とは Developer Experience*1 の略語のことです。大まかに言うと、日々の開発が健全に回せているかどうかを表現するもので、良し悪しを測るもの(良い DX = 開発に健全に取り組めている、悪い DX = 開発に健全に取り組めていない)です。

私たちは普段、コードに様々な変更を加えることでプロダクトを作っています。変更の理由は様々ですが、どれもプロダクトを成長させるために必要なことをしているはずです。ただ、それが本当に成長に寄与するかどうかはリリースしてみなければわかりません。そのため、1週間から2週間程度の期間で区切って定期的にリリースをすることで、それが狙い通り機能しているかどうかをチェックするプラクティスがうまれ、実践されています。

この1週間から2週間程度の期間というのはプロジェクトやチームによって様々ですが、この期間で設計をし、実装からQAまで完了するには、日々のワークフローを素早く健全にまわしていく「良いDXのため」の基礎体力やカルチャーが必要で、かつこれを持続的に取り組んでいくことが必要になります。

このセッション(記事)では、私たち開発者が日々使っているツールやサービスなどを活用しながら、良い DX を支え続けていくためのポイントを抑え、チームでの取り組みに落とし込むための指針を示していきます。

良い DX を支える技術: 事前準備

まずはじめに、チームの中での認識をあわせておくことがあります。たとえば、コーディング規約やブランチの運用方法などがあげられます。

コーディング規約

コードの書き方についてチームで認識を揃え、誰がどのコードを見てもある程度の読みやすさを確保します。

タブインデントかスペースインデントか、一行の長さは何文字までかといったスタイルから、Kotlin や Java での式の書き方など言語ごとの記法についても決めておきます。特に言語ごと特有の記法については、その記法に決めた理由や目的も合わせて明確にしておきましょう。

規約について決めたら、.editorconfig を使って誰でもおなじスタイルでコードを記述できるようにしておきます。Android Studio をはじめさまざまな IDE やエディタは自動でこのファイルを読み取り、スタイルを適用してくれます(プラグインを入れることで対応できるものもあります)。また静的解析ツールもこのファイルを参照できるので、CI で静的解析を実行するときにも利用できます。

次にプロジェクトのルートディレクトリに配置する.editorconfigファイルの例を示します。この例では、すべてのファイルについてインデントはスペース2つで、末尾の空白を取り除くなどを設定し、Java や Kotlin のソースコードについては 1 行につき 140 文字まで許容する設定になっている一方、Markdown では末尾のスペースにも意味がある(スペース2個で改行する)ので、特別に取り除かない設定をしています。

root = true

[*] // apply the following styles to all files
indent_style = space
indent_size = 2

end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.{java,kt,kts,xml}] // only apply to java/kt/kts/xml files
max_line_length = 140

[*.md] // only apply to markdown files
trim_trailing_whitespace = false

ブランチ戦略

ここでは Git を使うことを前提としますが、ブランチ戦略としてブランチごとに役割を定め、ワークフローを作っていくことで、差分管理を整理します。

Git には Git flowGitHub flow の2つのよく知られたブランチ戦略があります。もちろん、これ以外のブランチ戦略を採用するのもありですが、大事なことは、どのブランチにはどんな差分をコミット・マージすべきかを共有しておくことです。

たとえば、Git flow には master、develop の 2 つの主要なブランチ(GitHub などでは Protected branches に指定するような、全員の差分をまとめておくブランチ)と、作業ごとに使い分ける release、hotfix、feature ブランチがあります。develop ブランチは普段の機能開発用の差分を持っており、リリースに必要な差分が揃ったら release ブランチを作ります。release ブランチでは QA などで見つかったバグを修正する差分のみを保持し、すべてのバグ修正がおわったら master ブランチと develop ブランチにマージします。登場するブランチが多く管理コストはかかりますが、特定のリリースへ向けた差分を管理しやすく、モバイルアプリケーションのリリースライフサイクルとの相性が良いブランチ戦略です。

f:id:KeithYokoma:20200330191311p:plain
Git flow

一方で GitHub flow は Git flow を簡素化し、master のみをを主要なブランチとして差分を管理します。リリースも常に master から行うので、master にある全ての差分がリリース可能な状態であることが求められます。いつでもリリースしてよい状態が保たれることが前提となるので、どちらかといえば日々継続的にデプロイするサーバーアプリケーション向きのブランチ戦略です。

f:id:KeithYokoma:20200330191332p:plain
GitHub flow

あるいは、Git flow と GitHub flow の中間のようなブランチ戦略を考えても良いかもしれません。Git flow では master ブランチにリリース可能な差分を持っており、master ブランチのコミットはタグとほぼ一致するはずです。このタグを打つ場所を、release ブランチや hotfix ブランチの最後のコミットとしてよいのであれば、master ブランチと develop ブランチはひとつにまとめられます。

f:id:KeithYokoma:20200330191350p:plain
Composition of Git flow and GitHub flow

良い DX を支える技術: 設計

設計と一言で言っても、さまざまな考え方があります。クリーンアーキテクチャ、レイヤードアーキテクチャといった大枠の考え方もあれば、MVC や MVP、MVVM などプレゼンテーションにフォーカスした考え方もあります。いずれにしても大事なことは、クラスごとに与える責務をうまく定義して、その責務をユニットテストで検証することです。特に Android アプリなどのモバイルアプリでは、次の 3 つが設計を考える上での大きなトピックになります。

  1. API 通信やビジネスロジックを画面の実装から分離したい
  2. 画面が持つ状態と、その状態遷移をうまく表現したい
  3. 状態に応じた UI の変更ロジックを分離したい

どのトピックでも、それぞれに設計パターンがあるので、もうすこし掘り下げて解説します。

設計パターン

API 通信やビジネスロジックを画面の実装から分離する

MVC、MVP、MVVM ともにプレゼンテーションにフォーカスした設計パターンで、どのパターンでも Model と呼ばれる場所にはさらにいくつもの役割が存在します。API 通信やビジネスロジックはまさにこの Model と呼ばれている役割のなかにあります。Model の役割は多岐にわたりとてもフワッとしているので、API 通信であればそれに沿った命名を、ビジネスロジックであればそれをうまく表現する命名をし関心を分離していくほうが、どこで何をしているのか明確になりテストも容易です。

そこで、プレゼンテーションだけではなく全体としてどんな役割があるかを整理した考え方としてレイヤードアーキテクチャやクリーンアーキテクチャなどの考え方を取り入れます。 レイヤードアーキテクチャでは API 通信のような箇所をインフラストラクチャ層やデータアクセス層と呼び、ビジネスロジックに相当する箇所をビジネス層やドメイン層と呼んで管理します。これによって、それぞれの層のユニットテストで何をチェックしたらよいかもうまく決められるようになります。

画面が持つ状態と、その状態遷移をうまく表現したい

たとえば、API から1つのデータを取得し表示する画面を考えてみても、その画面が持ちうる状態はすくなくとも API を呼び出す前の初期状態、API を呼び出している最中の状態、API が成功を返してきた状態、API がエラーを返してきた状態の 4つあります。またこの状態の遷移には一定のルールがあり、成功・エラーは必ず呼び出し中の状態から遷移する制約が考えられます。

画面が持つ状態をドメインオブジェクトのようなものとしてとらえ、その状態をどのように遷移させるかをドメインロジックのようなものとしてとらえると、状態を表すクラスと、状態遷移のロジックを書くクラスの2つを作り、状態遷移のロジックの正しさをユニットテストで検証できるとうまく設計できそうです。

これを仕組みとしてフレームワーク化したものが Redux や Flux です。これらはもう少し登場人物が多く作るものが増えていますが、根本の考え方は変わりません。これらを実装に落とし込んだものとして、DroiduxRxReduxK などがあります。

もし Redux や Flux のフレームワークが重厚長大と感じる場合には、Rx のオペレータで状態遷移のロジックを表現し、新たな状態を下流へ通知するような流れを実装しても、同じことができます。Rx から LiveData への変換をすれば、プレゼンテーションでの実装は更に簡素化できます。この実装例は DroidKaigi 2020 のスタッフを支える(はずだった)技術 - 運営スタッフ向けアプリ - Infinito Nirone 7 の「プレゼンテーションレイヤの設計」にかんたんな例を示していますので、詳しい解説はこの記事を参照してください。次に RxJava と LiveData を組み合わせ、Android Architecture Component ViewModel を使った実装を示します。ここでさらに SavedStateHandle を組み合わせると、Activity の破棄と復帰にも対応できますようになります(別途 State を Parcelable にする必要はありますが)。

// State オブジェクト
data class ScheduleViewState(
  val schedule: Loadable<List<Schedule>, FailureReason> = Loadable.Ready
)

// ViewModel
class ScheduleViewModel(
  private val scheduleDataSource: ScheduleDataSource // 行動予定のデータを取ってくるデータアクセス層のインタフェース
) : ViewModel() {

  // 実際には MutableLiveData を公開してしまったまま作り込んでいた。LiveData を公開し、MutableLiveData は公開しないほうがいい…
  val _state: MutableLiveData = MutableLiveData(ScheduleViewState())
  val state: LiveData<ScheduleViewState> = _state

  private val disposables = CompositeDisposable()

  fun loadSchedule() {
    // DataSource は RxJava で結果を返すようにしている
    scheduleDataSource.load()
      .observeOn(AndroidSchedulers.mainThread())
      .doOnSubscribe {
        _state.nextState { // ScheduleViewState の schedule を読込中状態に更新
          it.copy(schedule = Loadable.Loading)
        }
      }
      .subscribe({ result ->
        _state.nextState { // ScheduleViewState の schedule を読込済み状態に更新
          it.copy(schedule = Loadable.Success(result))
        }
      }, { exp ->
        _state.nextState { // ScheduleViewState の schedule を読込失敗状態に更新
          it.copy(schedule = Loadable.Failure(InvalidResponse(exp.message.orEmpty())))
        }
      })
      .addTo(disposables)
  }

  override fun onCleared() {
    disposables.clear()
    super.onCleared()
  }
}

この状態遷移をテストするコードを次に示します。ある操作をしたとき、期待した順番で状態遷移が起きることを LiveData で検証します。

class ScheduleViewModelTest : Spek({

  // テスト準備
  beforeEachTest {
    RxAndroidPlugins.setMainThreadSchedulerHandler { Scheduler.trampoline() }
    ArchTaskExecutor.getInstance().setDelegate(TestExecutor())
  }

  afterEachTest {
    RxAndroidPlugins.reset()
    ArchTaskExecutor.getInstance().setDelegate(null)
  }

  Feature("ScheduleViewModel#loadSchedule") {

    val dataSource: ScheduleDataSource by memoized(CachingMode.EACH_GROUP) {
      mockk<ScheduleDataSource>(relaxed = true)
    }

    // Schedule が正しく読み込めたケース
    Scenario("Load data successfully") {
      lateinit var viewModel: ScheduleViewModel
      lateinit var observer: Observer<ScheduleViewState>
      lateinit var changedStateSlot: CapturingSlot<ScheduleViewState>

      // ViewModel を準備して…
      Given("ViewModel with initial state and state observer") {
        changedStateSlot = slot()
        observer = mockk(relaxed = true) {
          every { onChanged(capture(changedStateSlot)) } just Runs
        }
        every { dataSource.loadApiData() } returns Observable.just("foo")
        viewModel = NotificationViewModel(dataSource)
      }

      // ScheduleViewState を observe し…
      When("Start observing states") {
        viewModel.state.observeForever(observer)
      }

      // 読み込みを実行すると…
      And("Start loading") {
        viewModel.loadSchedule()
      }

      // 初期状態 -> 読込中 -> 成功 の順で ScheduleViewState が通知される
      Then("State changes observed in the specified order") {
        verifyOrder {
          observer.onChanged(eq(ScheduleViewState(schedule = Initial)))
          observer.onChanged(eq(ScheduleViewState(schedule = Loading())))
          observer.onChanged(eq(ScheduleViewState(schedule = Success(listOf(Schedule(/* mock data */))))))
        }
      }
    }

    // ...
  }
})

class TestExecutor : TaskExecutor() {
  override fun executeOnDiskIO(runnable: Runnable) { runnable.run() }
  override fun isMainThread(): Boolean = true
  override fun postToMainThread(runnable: Runnable) { runnable.run() }
}

状態に応じた UI の変更ロジックを分離したい

状態をうまく表現できれば、プレゼンテーションの責務は「状態に対応して適切に UI を変更すること」になります。一方で、Activity や Fragment はこれ以外にもライフサイクルの管理などモバイルアプリ特有の責務もあります。ライフサイクル管理についてはどうしても切り離せないのでそのままにしておくとして、状態に対応して適切に UI を変更する責務は、何らかの形で委譲ができそうに思います。これは Jetpack の Data Binding を使ってもよいですし、単純な委譲パターンを実装するでもよいでしょう。ユニットテストで UI の更新ロジックを検証するなら、単純な委譲パターンを採用するとやりやすいでしょう。あるいは、View コンポーネントをまとめて取り扱う Custom View を定義し、Activity や Fragment からはその Custom View で取り扱っている型のオブジェクトを渡すような作りにするのもよさそうです。

モジュール構成

設計パターンを考える上で、どんなモジュール構成にするかも少なからず影響があります。

シングルモジュールであれば、設計パターン如何に関わらずモジュールは常に1つなので、構成については何も考えることはありません。マルチモジュールの場合は構成をどうするかについて共通認識が必要になります。 一方、マルチモジュールの場合にはモジュール間の依存関係を定義でき、依存関係のないモジュールにはアクセスできなくなります。必要なモジュールにのみアクセス可能な状態にしておくことができるのは、シングルモジュールにはないマルチモジュールの利点です。

では、マルチモジュールにするとして、どのようにモジュールを分割するとよいでしょうか。いくつかの方法が考えられます。

  1. レイヤードアーキテクチャのレイヤーごとにモジュールを作る
  2. 機能やドメイン単位でモジュールを作る
  3. 1 と 2 を組み合わせてモジュールを作る

どの分け方でも良いですが、分け方に応じてモジュールの命名規則を予め決めておくと、どのモジュールにどのコードがあるかがわかりやすくなります。また、モジュール間の依存関係についても、適切な依存関係と不適切な依存関係についてあらかじめ決めておきましょう。

次にレイヤードアーキテクチャに基づいてモジュールを分けたときの命名規則を示します。

  • アプリケーション本体: app
  • プレゼンテーション/画面: feature_**
  • ユースケース/ドメイン/ビジネスロジック: usecase_**
  • インフラストラクチャ/データアクセス: repository_**
  • 横断的な共通機能: common_**

命名規則を決めたら、モジュール間の依存関係の作り方についてルールを設けます。

このモジュール構成の場合、app モジュールはすべての機能について知っている必要があるため、すべての feature_** モジュールに依存することは適切であると決めます。

一方 feature_** モジュールは他の feature_** モジュールについて知っているべきでしょうか。これは画面同士のつなぎこみ(ナビゲーション)をどのモジュールで実装するかによります。feature_** モジュール同士を依存させない場合には、画面遷移のためのインタフェースだけを feature_** モジュールで定義し、その実装は app で行い注入する仕組みが必要です。feature_** モジュール同士の依存を許容する場合は、直接画面遷移の実装を feature_** モジュールに書けますが、モジュールが循環参照しないよう気をつける必要があります。どちらも、アプリケーションの設計が必要になります。

レイヤードアーキテクチャに基づいたモジュールの分け方では、同じレイヤー内のモジュールに依存することはなく、違うレイヤーにあるモジュールに依存するのは正しいとするのがスッキリしそうです。

f:id:KeithYokoma:20200330194916p:plain
Good dependency

f:id:KeithYokoma:20200330194935p:plain
OK

f:id:KeithYokoma:20200330194956p:plain
NG

これとは別に、機能横断的に使われるモジュールはどのモジュールからでも依存してよいことにします。ただし、機能横断的なモジュールは往々にして関心が膨らんでコード量がふえがちなので、取り扱う関心事(UI 向けのユーティリティ、API 通信のユーティリティなど)ごとにモジュールを分けておくと、インフラストラクチャのモジュールが UI の共通機能へアクセスしてしまうようなことが減らせます。

良い DX を支える技術: 開発プロセス

どんな設計パターンを採用するかが決まり、モジュールの扱い方も決まったら、アプリケーションの実装が始められますが、日々の開発ではコードを書く以外にも様々なタスクがあります。ビルドが成功するかどうかをチェックしたり、コードが事前に決めた規約に沿って書かれているか、あるいは意図が伝わりやすい書き方かどうかをチェックしたりなど、コードの変更がうまく機能し、品質が保たれていることをチェックをしているはずです。ここでは、それらの取り組みを持続的にまわしていくための技術を解説します。

ビルドの自動化

日々の開発の中で、自分のつくった差分がうまく動かせるかどうかは最低限自分の環境で試すはずです。

それが他の人の環境でも動かせるかどうか、あるいは他のビルド設定でも動かせるかどうかもチェックしておくと、差分を安心してマージできます。しかし、これをいろいろなブランチで手作業でやるにはとても手間がかかるので、差分が正しくコンパイルでき、正常に成果物が生成できることを自動化しましょう。

自動化の手段は CircleCI や TravisCI、Bitrise、Wercker、Jenkins など多種多様なサービスやソリューションがあるので、詳しいセットアップの方法はそれらのサービスのドキュメントを参照してください。

テストの自動化

ビルドの自動化ができたら、次はテストの自動化をしましょう。これで、テストコードで検証している範囲で、差分が意図したとおりに動いていることが確かめられます。多くの CI サービスではテストの結果を、コードをホストするサービス(GitHub など)に通知してくれるので、コードレビューをするときのチェック項目の1つとしてかんたんにチェック可能なものになります。

ただし、テストの自動化はとても重要ですが、プロダクトの成長とともにどうしても実行時間が長くなります。ビルドやテストなど自動化したものが5分ほどで終わるなら問題ないかもしれませんが、15分や20分かかるとなると、その待ち時間は少なくありません。実行時間の増加は避けられないことではありますが、その増加のしかたはできる限り最小限にとどめられるようにしておきましょう。次に、それを実現するいくつかの方法を示します。

  1. Gradle の並列実行オプションを使い、できる限り多くのプロセスでビルドする (e.g. Improving the Performance of Gradle Builds)
  2. 一部の CI サービスで利用できる分散実行のスキームを使い、テストを分散実行する (e.g. CircleCI での Android プロジェクトのビルド設定と自動化の工夫 - Mercari Engineering Blog)
  3. 時間に依存するテストでの待ち時間をへらす (e.g. RxJava で delay など時間に関するオペレータを使ったときのテスト - Infinito Nirone 7)
  4. 不要なテストをへらす

CircleCI におけるテストの分散実行

CircleCI にはコンテナを複数起動し、コンテナごとに別のテストを実行するための仕組みがあります。次の図では、マルチモジュール構成なプロジェクトにおける、単一のコンテナでテストを実行する場合の概念図と複数コンテナでテストを実行する場合の概念図を対比しています。

f:id:KeithYokoma:20200330195128p:plain

もしシングルモジュール構成で分散する場合は、CircleCI が提供している CLI ツールを使ってコンテナごとに実行すべきテストを決定できます。

マルチモジュール構成で分散する場合でも CircleCI の CLI ツールは利用できますが、ファイル単位で分散することになり、ツールの吐き出した結果からモジュール名を取り出して加工しないと、実行時にエラーとなります。このため、モジュールごとに分散させるほうが単純です。次に、どのコンテナでどのモジュールをテストするかを決めるロジックを示します。

// 連番で割り当てられるコンテナの ID。
val containerIndex = System.getenv("CIRCLE_NODE_INDEX")?.toInt() ?: 0
// 起動するコンテナの総数
val totalContainer = System.getenv("CIRCLE_NODE_TOTAL")?.toInt() ?: 0

// 指定された ID のコンテナでどのモジュールのテストを実行するかを絞り込む
val modules = project.subprojects
  .withIndex()
  .filter { it.index % totalContainer == containerIndex }
  .map { it.value }

// 絞り込んだモジュールで対しテストを実行する
modules.forEach { module ->
  "./gradlew :${module.name}:test".runCommand()
}

静的解析の自動化

静的解析の自動化では、はじめにチームで認識を合わせたコーディング規約にあったスタイルでコードが書かれているかを検証します。言語特有の記法や設計について静的解析で検証できることは少ないかもしれませんが、これはコードレビューで私たちの目でチェックし、議論していきます。

静的解析には ktlint や checkstyle、findbugs など様々なツールがあります。どれも一定のフォーマットに従って XML を吐き出してくれるので、その XML を Danger などに食わせると、GitHub などのサービスとうまく連携し、コード上にインラインコメントの形で結果をフィードバックできます。コードに対するフィードバックをインラインコメントにしておくと、どこが問題だったかが分かりやすくなります。

配信の自動化

アプリケーションのビルドやテストがうまく動くことはもちろん重要ですが、それと同じくらい、実際の端末で動作を確かることも重要です。ビルドしたアプリケーションを素早くインストールし確かめられるようにすることで、QA によるアプリケーションの品質チェックをはじめ、チームや社内でのドッグフーディングなどでも、素早くフィードバックをもらえるようになります。

リリース前のアプリケーションの配信には DeployGate や Firebase App Distribution、HockeyApp などが利用できます。ビルドの自動化で成果物ができたあと、これらのサービスに apk や aab をアップロードすれば、同じサービスを使ってアプリケーションを管理している人に通知がとび、インストールできるようになります。

様々なブランチのビルド結果をアップロードしておくと、社外の人との共同プロジェクトなどでもアプリケーションの配信ができるようになり便利です。

ワークフローの自動化

たとえば、Git flow を採用しているプロジェクトで、release ブランチにおけるバグ修正を定常的に master ブランチにマージし続けることを自動化しておくと、リリース完了後に release ブランチをマージするよりもコンフリクトする可能性が減らせます。ブランチ戦略にあわせた自動化の作戦が必要ですが、できるだけ小さく差分を取り込み、コンフリクトなどのリスクを最小化することを目的としましょう。

f:id:KeithYokoma:20200330200447p:plain
release ブランチの定常的マージ

この他、コードレビューのレビュワーを自動でアサインするため CODEOWNERS を設定したり、Pull Reminders を導入して未レビューの Pull Request を通知したり、Pull Request Template を設定して共通のフォーマットで差分の説明を書いてもらうようにするといった各種の設定も、ワークフローをうまく回していく鍵になります。

良い DX を支える技術: リリース

リリースはいつも緊張するものです。新しい機能が期待通り動作するか、バグ修正が別のバグを生み出していないか、気になることは数えだしたらきりがないほど出てくると思います。問題が起きないことに越したことはありませんが、仮になにか起きたとしてもすぐにそれを検知し対処できるようにしておくと安心して修正に取り組めます。また、Google Play Store での段階的公開以外にアプリケーションの機能の公開をコントロールする仕組みを持っておくと、傷が浅いうちにトラブルを解決できるようになります。

フィーチャーフラグ

フィーチャートグルなどとも言うそうです。機能の公開をコントロールするためのフラグで、公開・非公開の2値でコントロールする以外にも、ABテストのような複数パターンを分岐するものも考えられます。

フィーチャーフラグを使う場面としておおきく2つ想定しています。

1. 新しい機能を段階的に公開したいとき

新しい機能を段階的に公開する場合、フィーチャーフラグの値は変えたいときにいつでも変えられるようにするため、Firebase Remote Config などアプリケーションのリリースを伴わない手段が必要です。

2. 1回のリリースサイクルを超えた開発をしたいとき

1回のリリースサイクルを超えた開発をする場合、開発用にブランチを分けて開発終了時にマージする方法がオーソドックスですが、開発差分が大きくなればなるほどマージの作業が手間になります。こまめに master ブランチの差分を開発用ブランチに取り込む方法もありますが、コミット履歴が少し入り組んだ状態になってしまいます。もし機能開発がフィーチャーフラグによる出し分けのコントロールが可能な場合、はじめにフィーチャーフラグを定義し非公開状態にしておき、開発が終わったら公開状態にする差分をいれるようにすることで、開発用ブランチで大きな差分を管理する必要がなくなります。

次にフィーチャーフラグを用いた、リリースサイクルを超えた開発の例を示します。Freature A 機能はバージョン 1.3.0 にリリースをする予定のものですが、開発の始まりは1.1.0 のリリース前から始まっています。フィーチャーフラグを用いれば、1.3.0 の直前でフラグを書き換えるまで、1.1.01.2.0 のリリースでは Feature A は公開されません。

f:id:KeithYokoma:20200331115409p:plain
フィーチャーフラグを用いたリリースサイクルを超えた開発の図

これを実現する手段を次に示します。

  1. BuildConfig にフィーチャーフラグを定義する
  2. 新しい機能へアクセスするためのユーザインタフェースのつなぎこみ作業を最後に実施する
  3. 開発者向けの設定画面で機能の有効・無効を切り替える
BuildConfig にフィーチャーフラグを定義する

BuildConfig にフィーチャーフラグを定義する場合はシンプルに build.gradle に次のようなフィールドを生成するよう書き足します。

defaultConfig { {
  buildConfigField("boolean", "IS_FEATURE_A_ENABLED", "false")
}

f:id:KeithYokoma:20200331120746p:plain
BuildConfig を使う場合

新しい機能へアクセスするためのユーザインタフェースのつなぎこみ作業を最後に実施する

既存の画面に新しいボタンなどを設置し、新機能へアクセスするような作りの場合、そのボタンの設置をリリースしたいバージョンの前に行うことで、それより前のバージョンでは誰もその機能にアクセスできなくなります。直接フラグを利用するわけではありませんが、同様の効果がある例です。

f:id:KeithYokoma:20200331120805p:plain
ユーザインタフェースのつなぎこみ作業を最後に実施する場合

開発者向けの設定画面で機能の有効・無効を切り替える

開発者向けの設定画面で機能の有効・無効を切り替える場合は、次のスクリーンショットに示すような画面を作っておく必要があります。

f:id:KeithYokoma:20200331121129p:plain
開発者向けのフィーチャーフラグ切替画面の例

フィーチャーフラグを用いる場合の注意点

フィーチャーフラグを用いる場合、フラグで非公開になった部分はすべて既存機能から独立している必要があります。新しい機能の開発のために既存機能に影響のあるような共通部分の変更をしてしまうと、せっかくフィーチャーフラグで新しい機能を非公開にしていても、既存機能の動作が途中で変わってしまう可能性があります。このため、うまく既存機能から分離するための設計が必要になります。

f:id:KeithYokoma:20200331121600p:plain
共通部分の変更がないよう設計する必要がある

また Firebase Remote Config や BuildConfig にフラグを定義する場合、完全に公開がおわったらそのフラグを削除することを忘れないようにしましょう。また、フラグを有効化するタイミングを間違えないようにすることも重要です。これらはどちらも開発者の運用に委ねられるため、可能なら UI のつなぎこみ作業をもって公開とするような他の手段を先に検討しておくほうがよいです*2

リリーストレイン

規模の大きなチームの場合、一度のリリースでたくさんの機能を公開できるようになります。一方で、どこかの機能開発の進捗が全体のリリースに影響を及ぼすような体制だと、リリースの都度いつリリースするのかの調整が必要になります。この問題の解法の1つとして、リリーストレインがあります。

リリーストレインはリリーススケジュールを固定化し、リリースへ向けた準備のスケジュールも固定化します。これにより、固定されたスケジュールを動かすのではなく、機能ごとにリリースのタイミングを固定されたスケジュールから選択するようになります。もし予定していたスケジュールに間に合わない場合は、次のリリーススケジュールを待つことになりますが、全体のリリーススケジュールを調整することはなくなります。

モニタリング

新しいバージョンが無事機能しているかどうかをモニタリングし、問題があれば早期に気づくためのしくみを作っておきます。検知すべき問題にはアプリのクラッシュや ANR (Android Not Responding) のほか、クラッシュはしないがアプリの表示上でエラーと出るような例外ケースの頻発、機能の劣化(リグレッション)やパフォーマンスの劣化などがあげられます。

Crashlytics を使ってモニタリングをする場合、クラッシュや、Crashlytics で集計しているキャッチした例外の件数が著しく伸びた場合にアラートを発する仕組みが用意されています。また Crashlytics は Slack への通知の仕組みももっているので、Slack を中心にモニタリングすることができるようになります。

f:id:KeithYokoma:20200331124314p:plain
ベロシティアラートの設定

f:id:KeithYokoma:20200331124329p:plain
Slack への通知の設定

ロギング

Crashlytics で検知した問題を解決するためには、適切なロギングが重要です。Crashlytics のレポートに添付されている例外のスタックトレースだけで解決できることもあれば、例外が発生する直前でどんなことが起きていたかを知らないと根本的な解決に至らない問題もあります。

Crashlytics には例外以外にも、ログを残しておくための仕組みもあります。Timber などをつかってロギングのインタフェースを統一し、ログ出力の先を Logcat と Crashlytics に振り分けておきましょう。

また key-value ペアとして様々な状態を記録することもできます。アプリのもつ設定値(e.g. ある機能が有効かどうか、別の機能の設定値は1,2,3のうちどれか、など)やユーザの状態(e.g. チュートリアルが完了したかどうか、など)*3を記録しておくと、問題が起きる前提条件を絞り込むための情報となります。

まとめ

プロジェクトが始まってから、リリース後の運用までを総ざらいし、どんなことに注目するとよい DX が得られるかを解説してきました。ここで解説したことを順番に取り組むもよし、つまみ食いして一番困っていることから取り組むもよし、様々な応用のしかたがあると思います。

解説ではプロジェクトの始まりからリリースまでの順序で見てきましたが、すでにプロジェクトが走ってきている場面でも、よい DX を得る改善ができるはずです。大事なことは、現状のよいところや改善したいことの認識をチーム内で合わせておき、何から取り組んでいくかを明文化していくことだと思います。また、失敗は小さく、リカバリーを素早くできるような仕組みを作っておくと、なにか問題がおきても気持ちよく解決に取り組めるようになります。

*1:https://gfx.hatenablog.com/entry/2018/06/28/100103

*2:https://martinfowler.com/bliki/FeatureToggle.html の Release toggles are the last thing you should do のセクションを参照

*3:プライバシーポリシーとも関わる部分なので注意して設計しましょう