Infinito Nirone 7

白羽の矢を刺すスタイル

<merge> タグをつかったレイアウトのプレビューを期待通りに表示する

Issue

Fragment を使わず View をベースにしてレイアウトを組むと、Fragment と同じような役割をもった CustomView を作ってレイアウトを組みます。 このとき CustomView は何かしらの ViewGroup を継承することになりますが、そのレイアウトファイルのルートを<merge>にしないと無駄な ViewGroup がひとつ挟まってしまいます。一方で、<merge>をそのまま使うと、レイアウトのプレビューでは親が何になるのかわからないため、期待通りの表示ができません。

Solution

tools 属性に<merge>の親が何になるかを指定するtools:parentTagが AndroidStudio 2.2 から増えています。

<merge
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:parentTag="android.widget.LinearLayout">
    <!-- views -->
</merge>

tools:parentTagには親となる ViewGroup の名前を指定しますが、Android フレームワークの ViewGroup であっても FQCN をつかいます。またandroid:layout_widthandroid:layout_heightを設定しないとプレビューがうまく表示できないことに注意しましょう。

See Also

stackoverflow.com

Kotlin でかいた interface を Retrofit に食わせたときのエラーに対処する

TL; DR

Kotlin & @Body · Issue #1805 · square/retrofit · GitHub

問題

Retrofit で REST API のクライアント実装を生成する時、Kotlin で書いた interface を渡すとき、メソッドにジェネリクスを使った引数がいるとまれに次のようなエラーメッセージを吐き出してクラッシュします。

java.lang.IllegalArgumentException: Parameter type must not include a type variable or wildcard: java.util.List<? extends Something> (parameter #4)

たとえ Kotlin のコード上ではワイルドカードを使っていない場合でも、Java に変換する過程でワイルドカードに翻訳*1されることがあり、このような問題が発生します。

対処

@JvmSuppressWildcardsというアノテーションを使います。メソッドに対してこのアノテーションを付けることで、Java への変換時にワイルドカードを使わないようになります。

Robolectric 3.4 RC3 で dependencies の ArtifactId が変わります

Robolectric にはいくつかのサブプロジェクトがあり、それぞれに dependencies を追加する必要があります。Robolectric ではサブプロジェクト名が ArtifactId になっています。 そして 3.4 RC3 から ArtifactId が変更になったものがあります。主には、robolectric-というプレフィクスが取れたり、shadows-というプレフィクスが取れるなどの変更ですが、shadows-coreframeworkというようにガラリと変わったものもあります。

3.3.2 まで

dependencies {
  testCompile "org.robolectric:robolectric-annotations:3.3.2"
  testCompile "org.robolectric:robolectric-junit:3.3.2"
  testCompile "org.robolectric:robolectric-processor:3.3.2"
  testCompile "org.robolectric:robolectric-resources:3.3.2"
  testCompile "org.robolectric:robolectric-sandbox:3.3.2"
  testCompile "org.robolectric:robolectric-utils:3.3.2"
  testCompile "org.robolectric:shadows-core:3.3.2"
  testCompile "org.robolectric:shadows-httpclient:3.3.2"
  testCompile "org.robolectric:shadows-maps:3.3.2"
  testCompile "org.robolectric:shadows-play-services:3.3.2"
  testCompile "org.robolectric:shadows-multidex:3.3.2"
  testCompile "org.robolectric:shadows-support-v4:3.3.2"
}

3.4 RC3 から

dependencies {
  testCompile "org.robolectric:annotations:3.4-rc3"
  testCompile "org.robolectric:junit:3.4-rc3"
  testCompile "org.robolectric:processor:3.4-rc3"
  testCompile "org.robolectric:resources:3.4-rc3"
  testCompile "org.robolectric:sandbox:3.4-rc3"
  testCompile "org.robolectric:utils:3.4-rc3"
  testCompile "org.robolectric:framework:3.4-rc3"
  testCompile "org.robolectric:httpclient:3.4-rc3"
  testCompile "org.robolectric:maps:3.4-rc3"
  testCompile "org.robolectric:playservices:3.4-rc3"
  testCompile "org.robolectric:multidex:3.4-rc3"
  testCompile "org.robolectric:supportv4:3.4-rc3"
}

Wercker での Android アプリの CI を速くするポイント

CI の速さは開発のプロセスを高速化する上で重要です。Android アプリのビルドはどうしても時間のかかる部分が多く数分から十数分の時間を要してしまいますが、工夫次第では数十秒から数分の短縮が可能です。 プロジェクトの規模や構成にもよるので一概にすべて効果があるとは言いにくい部分もありますが、この記事で取り上げる幾つかのポイントを抑えておくと、CI の高速化に役立つと思います。今回は Wercker を使用した場合の高速化の方法を書きます。

1.ビルドキャッシュ

Wercker の場合、WERCKER_CACHE_DIRという環境変数にキャッシュのためのディレクトリ情報が入っています。ここにビルドキャッシュを放り込むことで、複数ステップで gradlew コマンドを叩いたときにあとで実行するコマンドが速くなります。また、dependencies の artifact をキャッシュすることで、次回以降のビルドで依存解決が高速になります。

ビルドキャッシュをWERCKER_CACHE_DIRに放り込む場合は、gradlew コマンドのオプションに--project-cache-dir=$WERCKER_CACHE_DIRを指定します。

dependencies の artifact をWERCKER_CACHE_DIRに放り込む場合は、ビルド後のステップで~/.m2~/.gradleWERCKER_CACHE_DIRに cp するか、ビルド前に~/.m2~/.gradleWERCKER_CACHE_DIR配下のディレクトリを指すようシンボリックリンクを作るかのいずれかの方法があります。

依存するライブラリが多くなればなるほど効果が出ます。最近は JCenter が不安定なのか dependencies がダウンロードできなくてビルドがコケるという事象が日に何度か起こっていて、その失敗を解消することにも役立ちます。

2.マルチモジュール化

巨大なモジュールをモリモリビルドするのではなく、小さなモジュールを並列でビルドするようにします。最近の Gradle プラグインはマルチモジュールのビルドについて改善が入っているので、並列ビルドで起こりがちな問題もある程度はうまくやってくれます。キャッシュが効くとなお速いので、ローカルマシンでのビルド時間のほうが改善するかもしれません。

3.メモリ割り当て

Androidアプリのビルドはとにかくメモリが重要です。何はなくともメモリだけは広く確保する必要があります。MacBook Pro など多くのラップトップマシンでは 16GB が上限となりますが、Wercker では(2017年6月現在のところ)コンテナごとにメモリの上限は設定していないようですので、思い切って 32GB 割り当てるなどという富豪的な使い方ができます。特に build.gradle で指定するdexOptionsdexInProcessを有効にしたとき、javaMaxHeapSizeが大きくないと時間がかかってしまいます。CIという環境変数trueのときは32gなど大きな数字を割り当て、そうでないローカルマシン等では8gなどになるような柔軟性があるとよいです。

dexOptions {
  dexInProcess true
  javaMaxHeapSize "true".equals(System.getenv("CI")) ? "32g" : "8g"
}

大抵の CI as a Service ではCI環境変数が用意されているはずです。サービスによっては環境変数の値が真偽値の場合とCIサービス名の場合とがあるので注意が必要です。 また、dexOptionsjavaMaxHeapSizeを指定しても、gradle.propertiesorg.gradle.jvmArgsに何も指定しないと「もっと大きなヒープを使わないと意味が無いぞ」という警告が出ます。併せて、次のようにJVMが使える領域を大きくします。

org.gradle.jvmargs=-Xmx33280M

ただし、この記述をそのままリポジトリに放り込むと、そんなにたくさんメモリのないマシンで困ることになります。できれば CI でだけ大きな領域を確保したいので、リポジトリに入れる gradle.propertis にはローカルマシンで確保可能な数字にしておき、CI ではビルドステップの前に次のようなコマンドを実行するようにして大きな領域を確保するようにします。

- script:
  name: set up environment
  code: |
    echo -e "org.gradle.jvmargs=-Xmx33280M\nandroid.enableBuildCache=true\norg.gradle.parallel=true\norg.gradle.caching=true\norg.gradle.configureondemand=true\n" > gradle.properties

4.テストの並列実行

アプリの機能が増えればその分テストも増え、テストを実行する時間も長くなります。testOptionsでテストを並列実行するための設定項目があるのでこれを使います。ただし、並列実行すると壊れるテストも中にはあるかもしれませんので注意してください。

testOptions {
  unitTests.all {
    maxParallelForks = 2
    forkEvery = 150
  }
}

maxParallelForksが何並列で動かすかを決めるパラメータで、forkEveryJVM を再起動するタイミングをいつにするかを決めるパラメータです。この設定の場合、2並列で150個のテストケースを実行するごとに JVM を再起動します。

併せて、テスト実行時のメモリについても設定しましょう。次の例では Java 8 のランタイムでテストを動かすことを想定しています。

testOptions {
  unitTests.all {
    maxHeapSize = '8192m'
    jvmArgs '-XX:MaxMetaspaceSize=8192M', '-noverify', '-Xmx8192M'
  }
}

5.ビルドステップの整理

大抵の CI as a Service では、ビルドそのものや個々のビルドステップごとにタイムアウトが設定されていて、一定時間を過ぎてもコマンドが終わらないと失敗とみなされます。このため、時間のかかるステップを小分けにしてタイムアウトを回避するような対策をとることがあります。一方で、ステップを小分けにすると、毎回 gradlew を叩いてプロセスを起こすので、少し無駄な時間がどうしてもできてしまいます。先述のビルドキャッシュである程度は改善できますが、これまでの高速化ポイントで速くなったステップは、タイムアウト以内に納まるのであればマージしましょう。自分の場合、assemble と test でそれぞれ別のステップを作っていましたが、それぞれタイムアウト以内に十分納まる時間で終わるようになったのでマージしました。

ツールド美ヶ原

先月たまたま申し込み期限ギリギリに申し込んだツールド美ヶ原に行ってきました。 激坂があり、それを乗り越えた先には雄大な自然!気持ちよさそう!!楽しそう!!!という勢いで申込んだわけですが、梅雨真っ盛りとあって天気は週間予報からあまり良くなく、実際当日の早朝は雨が降っていました。 開会式が始まる頃には雨もやんでいて、頂上付近は曇りと言われていました。ただ、スタートしてしばらくは雨に濡れたウェットな路面でしたので、注意深くいくことに。

スタートが切られて、いざっと思ったらいきなりすごい激坂でした。16%とか18%とかいう鬼のような坂をいきなりぶっこんでくるのはすごいですね。しかもグレーチングがところどころにあるので油断ならない…とおもっていたら、シッティングでペダルを踏んだときのトルクがかかる瞬間にグレーチングを踏んでしまったようで、ズルッと後輪がすべりました。幸い転ぶことはなく、最初の難関はなんとかクリアすることが出来ました。ただ、10%後半だった坂が10%前半に落ち着いたというだけで、キツイことには変わりないのですが😭美鈴湖手前でスタッフのおじさんが「もうすぐ激坂おわるよ!」と声をかけていましたが、踏めども踏めども10%とか13%とかの坂が続いて泣きそうでした😂

途中、クリートのネジが緩んでいることに気がついて、第二給水ポイントで締めなおすことに。ロングライド装備をほぼそのままヒルクライムにもっていったのが役に立ちました。おかげで重たかったですが😒 ツールド美ヶ原の給水ポイントは3つありますが、全部走りながら水のはいった紙コップを受け取れて、回収もバッチリスタッフさんが面倒を見てくれる親切設計でした。

そういえば、悪魔おじさんに扮した悪魔お兄さんが走っていました。途中で追い越したのですが、周りの人全てに「キャノンデール!アレ!アレ!」とか「ビアンキ頑張れ!」とかと声をかけていました。下山時にはゴール地点であとから来る人達も応援していて、悪魔お兄さんの本気度が伝わってきました。

最後の給水ポイントからはガンガン踏んでいきました。平坦路と若干のくだりもあってスピードに乗りつつ、無事にゴールしました。タイムは1時間46分だったようです。 ゴール地点では冷やしトマトの差し入れをいただき休憩後下山しました。

序盤からきっつい坂が続く厳しいイベントでしたが、また来年もきたいですね!

Again: AsyncLayoutInflater vs Litho #potatotips

前回 は実装が悪くて完全に Litho が負けていましたが、ある程度動かせるものができたので、potatotips で発表してきました。

speakerdeck.com

結論から言えば、これでもやはり AsyncLayoutInflater のほうが速いです。「Litho は既存の XML によるレイアウトを完全に置き換えるものではない」との言葉通り、適用箇所をきちんと見極めないと意味がないよね、ということを身にしみて感じました。

前回の計測で抱えた問題

Litho は RecyclerView を念頭に置いているため、今のところ単なる ScrollView に相当するコンポーネントを持っていません。LinearLayout 相当のコンポーネントに子 View を足しても、与えられた領域のなかに子 View がおさまるよう計算するだけで、これが前回の計測で問題となった点でした。子 View が多くなると、子同士がオーバーラップし始め、その分無駄に計算しまくるためです(ログにもそのようなメッセージが表示されます)。

どう改善したか

ScrollView を親にその下で Column を設定しました。これで画面内にびっしり Text が敷き詰められるようなことがなくなり、計算がシンプルになって高速化しました。

ただしまだ問題はある

スクロールすると、初回描画領域外にあった Text が見えません😂。

Robolectric の依存を事前に解決してテストにかかる時間を短縮する

起こったこと

Robolectric を使って JVM 上でテストを動かす場合、次のようなエラーログを目撃することが稀によくあります。

com.sample.SampleTest > sampleTestCase STANDARD_ERROR
    Downloading: org/robolectric/android-all/6.0.0_r1-robolectric-0/android-all-6.0.0_r1-robolectric-0.pom from repository sonatype at https://oss.sonatype.org/content/groups/public/

テストの実行時に依存を解決しに行くような挙動をしていることがわかります。CI の場合、最初に実行されるいくつかのテストでこのようなログが出てきます。 CI の特性上実行ごとに依存を解決することには問題がありませんが、テストの実行時に都度依存を解決するのではテストの実行時間に影響が出ます。昨今の CI as a Service を利用する場合は、タスクやステップの実行時間制限があるものもあり、気をつけないとテストの実行ステップがタイムアウトして CI が失敗してしまいます。 また、ダウンロードのたびに STANDARD_ERROR としてエラーログが吐き出されるのも精神衛生上あまりよろしくありません。

解決の方針

テストごとに必要な依存を解決するのではなく、テストステップの前にすべての依存をシュッと解決しておき、あとはテストを動かすだけの状態にできれば、純粋にテストステップはテストの実行時間だけになり、タイムアウトの可能性が減らせます。

解決策

ちょうど同じようなことを解決しようとしている issue が Robolectric のリポジトリに立っています。

github.com

この issue にあるコメントによると、プロジェクトルートにある build.gradle に次のコードを記述し、依存解決のタスクを追加します。

subprojects { project ->
    task downloadDependencies(type: Copy) {
        description "Downloads all dependencies."
        group "build"

        from {
            // Use of closure defers evaluation until execution time
            project.configurations
                .findAll { configuration -> configuration.canBeResolved }
                .collect { configuration -> configuration.resolvedConfiguration.lenientConfiguration.files }
        }
        into "$project.buildDir/dependencies"
    }
}

そして CI の設定ファイルに、追加した依存解決のタスクをテストステップの前にさしこみます。例えば WerckerCI であれば、次のように記述します。

build:
  steps:
    - script:
      name: "install dependencies"
      script: |
        ./gradlew downloadDependencies
    - script:
      name: "test"
      script: |
        ./gradlew testDebug

結果

CI のトータルの時間ではさほど変化がないか、微増となりますが、テストステップの実行時間は1分ほどの改善が見られました。

余談

最近 Wercker がかなり不安定になっていて世は大消耗時代です。