Infinito Nirone 7

白羽の矢を刺すスタイル

kotest 4.3.0 で Robolectric Extension が安定版になった

以前に kotest を使ったテストに Robolectric をあわせるときの注意点を書きましたが、このときはまだ Experimental な状態だった Robolectric Extension が 4.3.0 から安定版となりました。

blog.keithyokoma.dev

マイグレーション作業

安定版となったことで次にあげるマイグレーション作業が必要になります。

import 文の修正

RobolectricExtension および @RobolectricTest のパッケージが変わっています。

- import io.kotest.experimental.robolectric.RobolectricExtension
+ import io.kotest.extensions.robolectric.RobolectricExtension
- import io.kotest.experimental.robolectric.RobolectricExtension
+ import io.kotest.extensions.robolectric.RobolectricExtension

安定版リリースでの変更点

同一モジュール内での @RobolectricTest と通常のテストの共存

以前は同一モジュールで RobolectricExtension を有効化すると、そのモジュール内のすべてのテストで @RobolectricTest が必要でした。安定版では必要なテストクラスのみに @RobolectricTest を付与し、@RobolectricTest の有無に関わらずすべてのテストが正常に実行できるようになりました。

安定版リリースでも動作しないもの

BehaviorSpec が利用できない

以前と変わらず、Robolectric の初期化が二重に走ってしまいテストがコケます。

IsolationMode を InstancePerLeaf 以外にするとテストがコケる

以前と変わらず、InstancePerLeaf 以外では Looper の初期化が二重に走ってしまいテストがコケます。

Navigation Component で Build Variant ごとに Deep Link を定義する

Build Variant ごとに Deep Link (App Links) の host を変えたいとき、Navigation Component の navigation graph の定義をどうするかを示します。

<deepLink> では String Resources が使えない

navigation graph も XML で記述したリソースだと考えると、次のような定義は一見正しそうですが、生成される AndroidManifest を見ると全く予期しない結果になります。

<navigation>

  <fragment>

    <deepLink app:uri="@string/deep_link_uri" />

  </fragment>

</navigation>

{build_variant}/res/values/strings.xml に deep_link_uri を定義すれば、Build Variant ごとに Deep Link の URI が切り替えられそうですよね。上記の navigation graph から生成される AndroidManifest の intent-filter は次のとおりです。

<intent-filter>

  <action android:name="android.intent.action.VIEW" />

  <category android:name="android.intent.category.DEFAULT" />

  <category android:name="android.intent.category.BROWSABLE" />

  <data android:scheme="http" />

  <data android:scheme="https" />

  <data android:host="string" />

  <data android:path="/deep_link_uri" />

</intent-filter>

String Resource への参照そのものが URI だと解釈され、string という host で /deep_link_uri という path の URI で Deep Link を設定してしまっています。 Issue Tracker にこの問題の報告がありますが、どうやら <deepLink> での String Resource への参照はサポートしていないようです。代わりに、Manifest Placeholder のような仕組みを導入する予定があるようです。

navigation graph を Build Variant ごとに定義する

仕方がないので navigation graph の XML を Build Variant ごとに定義するしかありません。

navigation graph が十分にシンプルで小さいなら navigation graph 全体を Build Variant ごとに配置してもよいかもしれませんが、Deep Link の設定を切り分けるのであれば、Deep Link を持つ <fragment> だけを切り出した navigation graph を Build Variant ごとに用意し、それを main source set にある navigation graph から <include> するほうが良さそうです。

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

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