Quantcast
Channel: nashcft's blog
Viewing all 97 articles
Browse latest View live

DroidKaigi 2020 アプリ開発 contribute の思い出

$
0
0

私が Androidアプリ開発に携わるようになったのが2017年後半からで、それから迎えた DroidKaigi は 2018, 2019, 2020 と今年で3度目になる。今年は残念ながら中止になってしまった*1けど、自分にとっては公式アプリに初めて contribute した年でもあるので、そのことについての振り返りを残しておこうと思う。

DroidKaigi 2020 公式アプリ

github.com

DroidKaigi では Play Storeを見る限り2016からイベントのスケジュールアプリを毎年開発していて、大体開催1ヶ月前くらいにソースコードGitHubで公開され、オープンソースプロジェクトとして開発に参加できるようになる。2018, 2019の時はコードを眺めるくらいしかできなかったのだけど、今年はそろそろ contribute してみたいなと思っていたところに偶然いい感じの issue が流れてきたので contribute することができた。

送った pull request

今回送ったのは5つ。

github.com

github.com

github.com

github.com

github.com

以下それぞれの pull request について簡単にまとめる。

Automate Dagger injection & Use Fragment LayoutId constructor #618

最初に取り組んだもの。Activity や Fragment に対する injection を DaggerAppCompatActivityDaggerFragmentを使うのではなく、architecture-components-samplesにある以下の実装を使った方法に切り替えて、そのついでに Fragment の View 生成を androidx.fragment の 1.1.0 から導入された LayoutId constructorを使ってコンストラクタに layout の recource ID を渡す方法に変えたよ、という変更。

github.com

AppInjectorは、大雑把にいうと Activity や Fragment に injection 対象として指定した interface (ここでは HasAndroidInjectorInjectable) が実装されていたら injection を行う、という実装をした LifecycleCallbacks を Application や 各 Activity (の FragmentManager) に仕込むというもの。利用する側は AppInjector#initializeでApplication に対して ActivityLifecycleCallbacksを登録するだけで良く、 FragmentLifecycleCallbacksの登録は ActivityLifecycleCallbacksの中で行ってくれる。

実は会社で触っているアプリで AppInjectorを使っていて、使い方や機構などをそれなりに把握していたので、Dagger 関連の変更とはいえ私にとってはそこまで難しいものではなくラッキーなタスクだった。これのおかげで今回 contribute ができたといってもいいかもしれない。Fragment の対応は、issue を見つけたときの twitter上での会話とか過去のコミットを見た感じこれを通して本当にやりたかったことはこっちかなって思ってついでに変更した。これは pull request 分けても良かったなーと思っている。

余談だが最近 DaggerAppCompatActivityDaggerFragmentでもコンストラクタに layout ID を与えて View の生成をできるようにする pull request が merge されたので、次のバージョンで AppInjectorを導入しなくても各 Activity/Fragment で injection の処理を記述せず、かつ LayoutId constructor の機能を使えるようになるはず。

github.com

そういえば AppInjectorにライセンス表記とかのコメントを足すのを忘れている...

Groupie 2.7.2 #677

Groupie のバージョンを上げること自体は実際は手段の方で、目的は 2.6.0で導入された Item#hasSameContentAsを使って同値判定の処理を簡素化する、という変更。

Groupie は内部で DiffUtilを使っていて、 DiffUtil.Callback#areContentsTheSameの判定に2.5.xまでは equalsを使っていたので、同一の Item で中身も一緒の場合には再描画をさせたくない時は equalsの override が必要になっていた。これが Item の実装によっては割と悩ましい問題となることもあって、その辺については以下の記事が参考になる。

qiita.com

DroidKaigi アプリでは元々この equalsの実装のために EqualableContentsProviderという interface を用意していたが、Item#hasSameContentAsの登場によって equalsの override をしなくても DiffUtilにおける同値判定を行えるようになったので、EqualableContentsProviderまわりの実装を全てそちらに移した、という内容になる。ちなみに、 Item#hasSameContentAsのデフォルト実装は equalsを呼び出すだけなので、2.5.x 以前から 2.6.0 以降にアップデートする時に影響が出ないようになっている*2

Groupie は2018年末頃から半年強アップデートがなく死んだかと思われたが、昨年8月にメンテナが増え、それ以降はそこそこ活発に開発が進むようになった (最近はまた落ち着き気味だけど)。

github.com

Adjust line spacing of the description in about page #703

テキストの行間幅の設定が漏れてたので修正したもの。

Make pull request CI failed when test tasks failed #715

Pull request のCIで使ってた CircleCI のジョブが失敗してるし、失敗してるのに status check が passed で返ってきてしまうので、失敗した時は失敗と返すようにして、併せて失敗している部分を修正する、という pull request。ジョブの失敗の内、依存ライブラリを前もってダウンロードできてなかった方は結局 offline mode を外すことで対応したのだけど、これは依存を全てダウンロードするタスクを作って解決できたらよかったのだけどなあという思いが残っている。何か方法がないかGradle 力の高い人の意見を聞いてみたい。

Add style Theme.DroidKaigi.ActionBar for dark theme #732

OSSライセンスの一覧表示を Googleが提供している oss-licenses-pluginで実装していて、そちらで別の問題があって issueが作られていたのだけど、そこで一覧が真っ白になってると報告されてそれを修正した pull request。Dark theme 向けの Theme.DroidKaigi.ActionBarの定義がなかったので、dark theme になってテキストの色などが白系になったのに背景などに light theme の時と同じ白色が当てられてしまったため。

おわりに

最初の pull request を出す前は何か簡単なものでも1つくらいできればいいなあ程度に思っていたが、最終的に5個送っていた。1つ目が merge されたり awesome label を頂いたりして楽しくなったのもあるが、DroidKaigi それ自体だけでなくこの公式アプリ開発も期間中は多くの人が活動していて一種のイベント感があり、開発に参加しやすい雰囲気があると感じたのも一因だったように思う。これは Live 配信された opening talkでも話があったように、DroidKaigi アプリ開発で初めて OSSに contribute したという人が増えたというところからも感じられた。

これからの1年は DroidKaigi 界隈にとって難しい状況になると思うのであまり暢気なことを言うべきではないかもしれないが、またこうやって来年もアプリ開発に contribute できたらいいなあと思っている。

*1:8/22 に Kotlin Fest と一緒に DroidKaigi 2020 Lite として開催される https://youtu.be/IwHw7vrFwSE?t=4260

*2:ただし別の箇所で breaking change が発生しているので注意


Android: Data Binding 3.6.0 から ViewDataBinding の定義が変わった

$
0
0

今日 Android Studio 3.6.0 stable がリリースされた。

android-developers.googleblog.com

3.6 から導入された機能の一つに view binding という、data binding の layout 中の要素にアクセスする機能だけを抜き出したようなものがある。

developer.android.com

機能の詳細については公式ドキュメントを見てもらうとして、この機能で data binding の時のように生成される Binding クラスは ViewBindingという interface を実装している。

/* * Copyright (C) 2019 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * *      http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */package androidx.viewbinding;

import android.view.View;
import androidx.annotation.NonNull;

/** A type which binds the views in a layout XML to fields. */publicinterface ViewBinding {
    /**     * Returns the outermost {@link View} in the associated layout file. If this binding is for a     * {@code <merge>} layout, this will return the first view inside of the merge tag.     */@NonNull
    View getRoot();
}

(from: AndroidX Tech: Source Code for ViewBinding.java)

この ViewBindingだが、3.6.0 から data binding で生成される Binding クラスの親クラスである ViewDataBindingもこれを実装するようになっている。

3.5 まで (3.5.3): AndroidX Tech: Source Code for ViewDataBinding.java

publicabstractclass ViewDataBinding extends BaseObservable

3.6 から (3.6.0): AndroidX Tech: Source Code for ViewDataBinding.java

publicabstractclass ViewDataBinding extends BaseObservable implements ViewBinding

つまり、 ViewBindingViewDataBinding, それと data binding や view binding で生成された Binding クラスの関係は以下のようになる。

ViewBinding
 |
 + - < View binding の機能で生成された Binding クラス >
 |
 + - ViewDataBinding
        |
        + - < Data binding の機能で生成された Binding クラス >

それが何か、というと、 <T extends ViewBinding> (Kotlin だと <T: ViewBinding>) みたいな型パラメータを定義すると view binding で生成されたクラスだけではなく data binding で生成されたクラスも適用可能になるということで、view binding と data binding は1つのプロジェクトに混在させることができるので、ViewBindingに関する実装を書いた時はそれに data binding の生成クラスを放り込んでも問題ないか考慮しておいた方が良いよ、という話。

追記

******

$
0
0

From: CyberAgent (2019/03~2020/03)
To: U-NEXT (2020/03~)

出戻りです。

CyberAgentでは Androidアプリの開発をしたり Bitrise で CI を良い感じにしたり GitHub Actions で CI を良い感じにしたりしていました。U-NEXTでも引き続き Androidアプリの開発をしたり CI を良い感じにしたりすると思われます。

Groupie に View Binding サポートが追加された

$
0
0

先月出してた groupie-viewbindingを追加する pull request が今日 merge された。

github.com

早速これの入った v2.8.0 のリリースもされた模様。

Release v2.8.0 · lisawray/groupie · GitHub

ものとしては groupie-databindingほとんどそのままという感じで、型パラメータが ViewBindingになっているのと、View Binding には DataBindingUtil#bindみたいな任意の binding class のインスタンスを生成する方法がないのでそれについては BindableItem#initializeViewBindingを override して自分で書いてね、というところがAPI上の差異になる。

簡単な例としては以下のような感じ:

class MyItem(privatevaldata: MyData) : BindableItem<MyItemBinding>() {

  overridefun getLayout(): Int = R.layout.my_item

  overridefun initializeViewBinding(view: View): MyItemBinding =
      MyItemBinding.bind(view)

  overridefun bind(viewBinding: MyItemBinding, position: Int) {
    // ...
  }
}

また、プロジェクトのAGPが3.6.0以上だと、 以前ブログで書いた通りViewDataBindingViewBindingを implement するようになったので Data Binding も扱うことができ、 Data Binding と View Binding 両方使う場合は groupie-viewbindingだけ依存を追加すればよくなっている。

ただこれまでに groupie-databindingを使ったことがある人には groupie-viewbindingで Data Binding を扱う時にちょっと注意点があって、groupie-databindingBindableItemは ViewHolder が bind される時に内部で ViewDataBiding#executePendingBindingsを実行してくれていたのだけど、 groupie-viewbindingのはそれがなくなっている。なので binding class に対応する layout が持つ変数や observable object を変更することで view の更新を行う場合には、 override する bindの実装の中で自分で executePendingBindingsを実行するのを忘れないようにしなければならない。 groupie-databindingから groupie-viewbindingに移行する場合は既存の BindableItemを継承した class 達にはすべて initializeViewBindingの override をして回る必要があるので、その時に確認を忘れないようにしてほしい。

developer.android.com

stackoverflow.com

あと、今回の groupie-viewbindingの追加で groupie-databindingが deprecated になった。近い内にメンテも止める予定だそうだ。まあ確かに groupie-viewbindingが Data Binding も扱えるのでそっちに寄せてしまおうというのはわかるのだけど、新規はともかく groupie-databindingからの移行となるとユーザにはやや不便を強いることになり、その点については心残りがある。

まとめ

  • View Binding サポートの groupie-viewbindingが追加された。Data Binding とも使える
  • groupie-databindingが deprecated になった。メンテも止まる予定
  • (groupie-databindingから移行するユーザ向け) groupie-viewbindingで Data Binding を扱う場合の変更ポイント:
    • Binding class のインスタンス生成のために initializeViewBindingが必要
    • bindの実装の中で layout に持たせた変数や observable objects を経由して view の更新を行っている場合は、最後に executePendingBindingsを実行する

CircleCI to GitHub Actions 移行日記

$
0
0

職場の AndroidアプリのCIに CircleCI を使っていたのだけど、古いプランのままでコンテナのRAMが4GBの medium しか使えないのと、アプリの規模が大きくなりつつあるために色々対策をしてもOOMで落ちることがままあり、幾らかお話をした結果として現状簡素なCIしか無いし欲しいのは十分なRAMくらいなので CircleCI のプランを現行の performance plan にするくらいなら GitHub Actions の方がコスパが良いということで移行をしている。

先にビルドがより不安定な project から移行をして、そちらは問題なく移行できたのだけど、最も安定していた project を移行した際に gradle task の実行中に特に中断されるでもなく応答が返ってこなくなるという挙動に遭遇した。よくあるCI用の実行オプションを足しても特に改善される様子がなかったので debug log を出力させてみたところ、 task の実行途中で空き容量を超えた量のメモリ確保の要求がきて、解放しようとしたが殆どもしくは全く空けられず、延々と request が飛んでくるだけといった状況になってることがわかった。

関連する log は以下のような感じ:

[DEBUG] [org.gradle.process.internal.health.memory.DefaultMemoryManager] 728290099 memory requested, 429182976 free
[DEBUG] [org.gradle.workers.internal.WorkerDaemonExpiration] Will attempt to release 694 of memory
[DEBUG] [org.gradle.process.internal.health.memory.DefaultMemoryManager] 728290099 memory requested, 0 released, 429182976 free
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Waiting to acquire shared lock on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Lock acquired on daemon addresses registry.
[DEBUG] [org.gradle.cache.internal.DefaultFileLockManager] Releasing lock on daemon addresses registry.
[DEBUG] [org.gradle.process.internal.health.memory.DefaultMemoryManager] 728290099 memory requested, 429182976 free
... (以下繰り返し)

Gradle task 実行時のオプションは以下の通り:

 -Dorg.gradle.jvmargs="-Xmx5120m -XX:+HeapDumpOnOutOfMemoryError" -Dorg.gradle.workers.max=2 -Dorg.gradle.daemon=false -Dkotlin.incremental=false

要求されるメモリの量は常に同じなのでずっと同じ要求が繰り返されているんだろうなと思うのだけど、何のために700MB程度確保しようとしているのか、何故できてもほんの少ししか解放できないのかなどわからないことばかりだし、じゃあアプリの規模に対して -Xmxが小さすぎるのかというと見ての通り少なくない、というかむしろかなり多い方だし、大きくなりつつあるとはいえ特別大規模なアプリではないのでカツカツということはないと思っているので、今の自分の gradle に対する知識ではお手上げだなあとなっている。あとこの gradle task が進まなくなる挙動は必ず発生するわけではなく時々完走するので、何か変更を入れたときにたまたまビルドが通るとそこに原因を求めてしまいがちで調査がなかなか進まずもどかしい。

Android Gradle Plugin 4.1.0 から library module の BuildConfig に VERSION_CODE と VERSION_NAME が生えなくなりそう

$
0
0

Android Gradle Plugin 4.0.0 までは library module にも生えていた BuildConfig.VERSION_CODEBuildConfig.VERSION_NAMEが 4.1.0-alpha05 から生えなくなってた

issuetracker: https://issuetracker.google.com/issues/154275579

Library module にとっては不要でしょ、とのこと

Version Code mean nothing for Libraries. It's actually potentially confusing as one might expect that the BuildConfig.VERSION_NAME of a library is set to the version name of the app but this was never the case. For these reasons, it's better to not include it in library modules.

AGP 4.1.0-beta01 で試しても同様だった

App module が依存している library module 内で生成している BuildConfigVERSION_CODEVERSION_NAMEには app のバージョン情報が適用されてて、現在そこから library module 内でバージョン情報にアクセスしてるようなアプリはDIでバージョン情報を使うもの/取得できるものを app から配るようにしないとダメそう

CircleCI の Android Docker Image でJDKが11にアップデートされた件のまとめ

$
0
0

私は今回の件より前から CircleCI を使っておらず、最近は主に GitHub Actions でCIを構築していてこの件で被害は被っていないのだけど、軽く調べてみたところ Android project を JDK 9+ でビルドすることに強い興味をを持っている自分には結構興味深いことがわかったのでもう少し詳細に調べてまとめることにした。

何があったの

日本時間で 2020-08-18 のお話

  • CircleCI の Android Docker Image が更新され、JDK 8 ベースから JDK 11 ベースになった*1
  • これらの image を使ってCIを行っている Android project でビルドが失敗するものが発生した
  • Twitterが少し賑やかになった

原因1: なぜビルドが失敗するようになったの

この blog を書き始めてから調べて知った付け焼き刃な部分もあり正確な説明になっていない部分もあると思うが (詳しい方指摘ください...)、大雑把には JDK 9 で導入された Project Jigsaw*2と呼ばれる module system に関する変更によって標準の package に破壊的な変更が加わり、それによってこれまでデフォルトでアクセスできた package にアクセスできなくなったことが原因である。

具体的には

  • DataBinding -> JAXB
    • JDK 9 でJAXBの package は deprecated となり、デフォルトで解決可能な classpathから外された*3
    • JDK 11 でJAXBを含む Java EEAPIJDKから削除された*4
  • JaCoCo ->jdk.internal.reflect
    • JDK 9 でデフォルトでアクセス不可能になった*5

という感じ。

原因2: なぜJDKのアップデートが行われたの

この変更は予告なく突然行われたわけではなく、実際には7月から変更のアナウンスが forum で発表されていた。

discuss.circleci.com

この投稿から辿っていくと、以下の issue がきっかけのように思われる。

github.com

この issue は targetSdkVersionを29にしているため Robolectric を使って実行するテストが Java 8 では動かないので、Javaのバージョンを上げてくれというものだ。これは Robolectric 4.3 から適用される制約で、 release note で簡単に触れられているが、SDK 29 以降で Robolectric を使用する場合には runtime に Java 9 以上が要求され、Java 8 で実行するとエラーで止まるようになった。

https://github.com/robolectric/robolectric/releases/tag/robolectric-4.3

Running tests on Android Q requires Java 9.

https://github.com/robolectric/robolectric/releases/tag/robolectric-4.3.1

Running tests on AndroidAPI 29 now strictly requires a Java9 runtime or newer.

ところでこの問題自体は JDKのバージョンを上げなくても回避可能で、テスト実行時のSDKのバージョンを28以下に指定すればよく、 robolectric.propertiesに以下の設定を追加すれば良い:

# 28未満でも良いsdk=28

これによって robolectric.propertiesが配置されている module での Robolectric を使ったテストでは指定した SDK version に基づいてテストを実行してくれる (この場合でも警告は出力される)。この指定をした場合でも、個別に @Config annotation で指定した SDK version が優先されるので影響は出ない*6

大半の場合はこれで解決するのでJDKのバージョンを上げる必要はないのだが、一つだけ上記の回避方法では解決できずに Javaのバージョンを上げなければ解決しないケースがあり、それは minSdkVersionが29以上のときである。このケースでは robolectric.propertiessdkのバージョンを下げてしまうと targetSdkVersionminSdkVersionを下回る状態になってしまい下記のようなエラーになる。

java.lang.RuntimeException: android.content.pm.PackageParser$PackageParserException: <path to apk for local test>: Requires newer sdk version #29 (current version is #28)

解決方法

動かせた頃の image を使う

CircleCI Japan のアカウントで紹介されているように*7JDK 8 ベースだった時の docker image の digest を、使用する image の指定に追記することで使う image の固定ができる。今回の件で知ったが、これは document でも best practice として言及されている*8

docker:- image: circleci/android:api-29@sha256:<digest>

JDK 11 でもビルドできるように project を修正する

DataBinding

公式のアナウンスはまだなかった気がするが、 Android Gradle Plugin 4.1.0 で修正されているらしく、そちらでは JDK 11 を使っても DataBinding 関連のビルドが成功するようになっている*9*10。手元で動かしてみた感じでは 4.0.0, 4.0.1 でも同様にビルドできたので、4.0.0 にも backport されているらしい。

また、何らかの理由で AGPを 4.0.0 以上に上げられない場合は、JAXB関連の依存を自分の project に追加することでも解決できる:

dependencies {
  annotationProcessor 'javax.xml.bind:jaxb-api:2.3.1'
  annotationProcessor 'com.sun.xml.bind:jaxb-core:2.3.0.1'
  annotationProcessor 'com.sun.xml.bind:jaxb-impl:2.3.2'// kapt を使っている場合以下も追加する
  kapt 'javax.xml.bind:jaxb-api:2.3.1'
  kapt 'com.sun.xml.bind:jaxb-core:2.3.0.1'
  kapt 'com.sun.xml.bind:jaxb-impl:2.3.2'
}

JaCoCo

JaCoCo の設定に excludesという coverage 対象から除外するパターンを追加できるものがあるが、それに以下のように jdk.internal.から始まる package を追加することで解決可能*11*12

tasks.withType(Test) {
  jacoco {
    includeNoLocationClasses = true// excludes の一覧に 'jdk.internal.*'を追加
    excludes = ['jdk.internal.*']
  }
}

余談: JDK 11 になって嬉しいこともある

CircleCI で Java project といえばOOMだったりCPUの core を container に割り当てられた数よりも多く認識してしまったりという問題をどう対処するかという話があり、公式でも幾らかの言及がある*13*14。これはJVMの挙動の都合で cgroup による割り当て設定を見ないことが原因の一つなのだが、Java 10 で UseContainerSupportという VM option がデフォルト有効で導入され、cgroup で割り当てられた設定が反映されるようになり、また同じく追加された MaxRAMPercentageで割り当てられたRAMの量に対するパーセンテージで heap size の上限を定められるようになった。

この辺の話は本当に詳しくないので適当に記事のリンク貼ってお茶を濁させて。

www.eclipse.org

developers.redhat.com

merikan.com

medium.com

というわけで、もともと JDK 11 でもビルドできたり、先に述べたような対応をして JDK 11 でもビルドできるようにしたりした project は、上記の VM option の恩恵を得られるようになる。とはいえ昨今のCIを構築している Android project は大体が対策として Gradle の worker の数を絞ったり Xmxを設定したりしていると思うので、今なおRAMやCPUの問題で苦しんでいる人はそれほど多くないような気がするし、それらの対策のためにわざわざ JDK 11 対応をするにしてもモチベーションとしては弱いかもしれない。

雑感

この blog を書き始めた当初は AGP 4.0.0 以上でも JDK 11 で DataBinding がビルドできないものと思っていたので、こんな影響評価のお粗末なサービスは使わないで他のCIサービスに移行した方が良いですよみたいな締めくくりにして、ついでに移行先候補についても幾らか書こうと思っていた程度には個人的には印象が悪かった。その後調べた結果として DataBinding を捨てるなど大規模な修正なしに Android project 側でも対応可能なのがわかって、そこまで言う程ではないという評価に変わってその部分は削ったものの、実際はどうあれ CircleCI (の Androidまわりに関わっている人) には JavaAndroidの情勢に詳しい人はいないんだろうなあ、という不信感は残った。

発端がユーザからのリクエストで、それに素早く対応する姿勢はとても好ましいものだと思う。しかし解決したいこと (Robolectric 4.3 以上を動かせるようにすること) に対してとった手段 (JDK 11 へのアップデート) によって発生した影響が釣り合っていないので、ちゃんと JDKのアップデート内容やそれによって起こりうる影響の調査をしたのだろうかという疑問がある。彼らは docker image の更新をリリースした当時 JDK 9 でJAXBのAPIにアクセスできなくなることが DataBinding のビルドに影響を与えることを知らなかったのだろうし*15、もしかしたら実行時の AndroidSDKのバージョンが29以上であることが必要でない限り Java 8 でも Robolectric 4.3 以上を使ったテストを実行できるようにする方法があることを今でも知らないかもしれない。そもそもまだ Android Studioに組み込まれている JDKは4.2 Canary でも JDK 8 で、Androidアプリ開発の現場の大半では開発環境に JDK 8 を想定していると見て良いはずなのに、1つのライブラリに対応するために自分たちのメインストリームをそこから外していく判断はちょっとよくわからなかった。CircleCI としては過去のバージョンを digest 使って固定できるし、それを将来の変更に備えるための best practice として紹介しているという言い分はあると思うけど。

それはそうと Java 8 のリリースは2014年と6年も前のことであり、現在の Javaの最新は14, 来月には Java 15 がリリースされ、来年2021年には Java 11 の次のLTSである Java 17 がリリースされる予定である*16Java 8 までと Java 9 以降でリリースサイクルが変わったとはいえ、様々なアップデートが行われた、また行われている Javaにおいて8はもはやレガシーな環境だと言って良いと思われる。Android OS それ自体は Javaそのものを使っているわけではないのでAPIの追従に時間がかかるのは仕方ないことだが、ビルドや local test の実行に関しては Java後方互換性のおかげで結構なんとかなるし*17、現時点で最も困難な (と自分は思っている) Java 9 の壁を越えられるようになったことがわかったので、今後は Androidに対して新しいJDKへのサポートを積極的に要求していっても良いのかもしれない。

www.oreilly.com

www.manning.com


Android Studio 4.2 から組み込まれる JDK のバージョンが 11 になるっぽい


FlexiSpot 買って組んだログ

$
0
0

引越したのでデスクを新調した。会社では okamura の Swiftを使っていて体験がとても良く、自宅にもスタンディングデスクを導入したいと思っていたのでこれを機に FlexiSpot E3を購入。天板はかなでもののラバーウッドにした。

f:id:nashcft:20201126220045j:plain
完成後に適当にものを配置した時の様子

購入と準備

天板

当初の予定ではあまり工作をしたくなかったので FlexiSpot で天板セットを購入しようと思っていたのだが、購入当時欲しかった最大サイズの天板 (1600x700)が入荷待ちとなっていたので慌てて別の候補を探し、自分でやすり掛けや塗装などをしなくてよいいという理由でかなでものから購入することにした。この時に天板のサイズが大きくなった (1800x800)。

工具

購入後すぐに「flexispot 自作」「flexispot かなでもの」などで検索しまくって FlexiSpot のデスクを組み立てた先人たちの記録や天板にかなでものを選択した場合の留意点を漁ったり、会社の同僚からアドバイスを貰ったりしつつ準備をした。主に参考にしたものは以下:

パワーが必要とはいえ集合住宅なのでインパクトドライバは騒音的に避けた方が無難かなと思ったこと、スペック的には nabetama さんのブログにあった makita の DF333DSHX が良さそうだと思ったが amazonで配送日指定ができなかったこと、あとラバーウッドの硬さについてろくに調べずに、かなでもののサイトに「うちで扱ってる木材で一番硬い」と書いてあるのを読んでビビったことなどから、電動ドライバドリルは makita のDF483DRFXを購入した。ビット類はプラスドライバのビットが1つついてくるだけなのでいくつか適当にドライバビットとドリルビットも購入した。

マット

脚+天板で数十kgするし、流石に下に何か敷いておこうということで適当なマットを購入して敷いた。重量のあるパーツを置いた時の接地面がかなり小さいみたいなこともあったので組み立て作業時にも何かしら敷くものがあった方がよさそう。

組み立て

脚の組み立ては特に難しいことはなくすぐ終わった。昇降ユニットが重いので取り回しに注意した方が良い程度。ビームの長さは天板の幅1800mmに対して1600mmにした。

下穴を空けるのはドライバドリルの形状的に脚を天板に置いたままではできない箇所があったので、印をつけてから脚をずらして行った。最初の方は天板を貫通させないか心配で仕方なかったのでほんの少し削っては棒を挿して深さを測るみたいなことを何度も繰り返してて全然進まなかったのだけど、4, 5箇所空けたら感覚掴めてきたのでそれ以降はすんなり進められた。ところで世の中にはドリルストッパーとか深さ調整アダプタとかいう商品があるのでみなさんはそれを使いましょう。私はこれを書いてる最中に存在を知った。

最も気を揉んでいたネジ締めだが、電動ドライバで締める前に家にあったドライバで仮止めのような感じで軽く締めていたら思ったより感触が軽く、もしやと思いそのまま締め続けていたら最後まで苦労なく締めることができてしまった。とはいえ自分で回さない分電動ドライバの方が楽なんだろうと思ってそちらでも締めてみたが、ネジに掛かっている力の加減が掴めずビットがネジから外れたりネジ穴を潰したりしてしまわないかという不安が終始付き纏ったのと、その不安のため低速で締めていて時間短縮にならなかったので、じゃあ手で締めた方が気楽じゃんということで結局殆ど電動ドライバは使わなかった。これは慣れない*1道具だったのが原因だと思うので、使い慣れていれば電動ドライバの方が早くて楽に作業できるのかもしれない。

(空けてたか ->空けてたが)

という感じであとはケーブル繋いでケーブルカバー取り付けて組み立て終了。

f:id:nashcft:20201126220057j:plain
組み立て終わってひっくり返す前のデスクと、かっこいいのに思ったより活用できなかった makita のドライバドリルセット

ひっくり返す際は、天板取り付け後なら他の自作ブログでもあるように少なくとも二人で作業した方がよさそう。今回は下にマットが敷かれてて、天板がマットからはみ出ていたので天板の下の隙間に指を引っ掛けて転がす要領で持ち上げることができたけど、床に直接置いてあった場合なんかは脚部分を引っ張り上げるみたいな感じになって、ネジを締めた部分に負荷かかって天板の重量によっては破損しそうだなって思った。

組み立てと設置に関するその他所感

  • 下穴は2.5mmでOK, 3mm だと少し大きいかもしれない
  • FlexiSpot に付属してたプラスドライバはNo.1っぽかったけどNo.2の方がフィットした
  • ビーム部分のネジ締めはネジ穴が凹型パーツの底にもあるので、ドライバビットにマグネット機能があると便利
  • 脚の接地面が面で支える形状になってないので少なくともパッドを挟んだ方がよさそう

使用感

  • 高さ変更時の動きはスムーズ
  • 高度を高くした時もそこまで揺れない
  • 高さ変更ボタンを押しっぱなしの状態にするのが難しい気がする、メモリ機能を活用した方がストレスがない

という感じで、ボタン操作以外はほとんど Swift と遜色ない感じだと思う*2。つまりたいへん体験が良い。あと広い天板はとても良いのでみなさんも FlexiSpot でデスク作るときは天板を無闇に広くしておくと良いと思う。

今後

  • 電源まわりを整備する
  • モニターを買う
  • MoonLanderを買う
  • デスクマットあった方がいいかも
  • 椅子も買い換えたい
    • エンボディチェアかセイルチェアがいいなあ

*1:電動ドライバドリルを使うのは今回が初めてだった

*2:半年以上オフィスに行ってないので Swift の使用感はうろ覚え

Cast SDK の KTX ライブラリを作っている

$
0
0

github.com

夏頃から社のアプリの Cast 機能まわりを触り始めてて、しばらくして本格的に機能開発に関わるようになったところで一旦きちんとAPIさらっておくかーと思ったのと、ついでに Kotlin サポート欲しいけどすぐ出てこなそうだから自分で作るかーってなったので作り始めた。

今のところ簡単なことしかやってなくて、とりあえず Builder class でのオブジェクト生成を scope function で書けるようにしたのと、play-services-cast-framework向けに RemoteMediaClientなどにある PendingResultを用いた通信を suspending function として呼べるようにした程度。 play-services-cast-tvの Modifier や Writer 向けにも scope function 使えるようにしようかと考えてるけどこの辺まだ殆ど触ってないので使用感がわからず手をつけてない。

Builder をネストできる箇所があって、そういう所は scope function で表現すると呼べる関数が混ざって事故るよなーとは思ってて、 @DslMarker使ってアクセスできる receiver の制限できないか試してみたけど、筋よく実装できなそうなので諦めた。

Type-Safe Builders - Kotlin Programming Language

外部のライブラリに対して @DslMarkerの機能を有効にするには、対象を継承した自作 class を作ってそれに @DslMarkerを適用した annotation class を当て、そちらの class を receiver にした lambda を受け取る関数を実装する必要がある。しかし Cast SDKの Builder class の中には LaunchOptions.Builderなど final class になってて継承できないものがあり、それらは wrapper class みたいなものを書かないと receiver の制限を実現できない。そんな class はメンテ面倒だし、 Builder class が継承可能というのも変な話で今後他の Builder もfinalを付けられていくのが妥当だろうと思う。

Cast SDK自体外から拡張できる要素はそんなにない印象だけど、今ある cast-sdk-ktxAPIは私が会社のアプリで読んだり触ったりした部分で使われてた機能に対して作れたものみたいなところがあるので、まだ手薄な部分がなくはないと思う。Cast 機能実装している各位の秘伝の拡張関数みたいなのがあったら pull request を出してくれると嬉しい。

本当は KTXライブラリは本体を開発してるところがメンテしてるのが一番良いと考えているので Googleに貰われないかなってちょっと思っていたりする。ないだろうけど。

Material Components for Android: ボタンテキストの色

$
0
0

前提として以下の記事がある:

y-anz-m.blogspot.com

大雑把にまとめると Material Components for Androidを 1.0.0 から 1.1.0 に上げたら AlertDialogのナビゲーションボタンの文字色が変わってて、コードを比較したらデフォルトカラーの参照先が colorAccentから colorPrimaryに変わってたよ、というお話。

この記事を読んだあと、なんで変わったんだろうっていうのが気になったので release notes や commit を辿ってみた。ちなみに私は Material Components for Androidは雰囲気でしか使ってないし Material Designのことをよく知らない。

変更があったのは 1.1.0-alpha01で、具体的には 15bf27dの commit。1.1.0-alpha01 の release noteを読むと Material Designの color system に合わせた color theming にしたとある。Material Design の Dialogs のページにある Theming の項を見ると、確かに Button Text は Primary となっている。

公式の 1.1.0 リリースのアナウンス記事も読み返した感じだと、1.1.0 以降は Material Components for Androidが提供する component や theme は Material Theming に則った attribute の適用をするからね、という風なので、前提知識として Material Designの guideline をおさえておく必要がありそう。

medium.com

2020年振り返り

$
0
0

去年もやってたし今年も。

転職

nashcft.hatenablog.com

前職を辞めた理由の9割は前職全く関係ないし、残りの1割も (会社側にとって) 殆どとばっちりのようなものなので、大変な時期に突然辞めるねってなってしまったことは申し訳なく思う。一方転職後は割と当初の思惑通りに過ごすことができたし、それが自分にとってプラスに働いてるように感じるので、振り返ってみると良い判断だったという自己評価にはなる。

現職は入社する前からリモートワークに移行してて、私も入社してからずっとフルリモートで働いている。オフィスには3, 4回しか行ってない。主にやってたことは以下の3点か。他は細々とした機能開発をしたりいつも通りテスト書いてリファクタリングしてバグを直したりみたいな。

  • 出てってから戻ってくるまでの間に荒廃したCIの立て直し
  • Google Cast機能関連の開発
  • メンバーのメンタリング

1つ目のCIの立て直しは、まあ反省するところもあって、以前いた時はずっと自分だけでCIをメンテしていて他のメンバーに触ってもらう機会を作っていなかったので、仕組みの周知や自分たちでメンテするものという意識づけができておらず、私がいなかった間は勝手のわからないオーパーツみたいな感じになっていたのが荒廃の原因だったのだと思う。立て直しも GitHub Actions 移行も兼ねて結局1人でやってしまったけど、今は幸いなことに興味を持ってくれたメンバーがいるので、試しに job を作ってもらうなどしてもらっている段階。

Cast は夏ぐらいからやってて、今月で一区切りついたと思う。元々別のメンバーが1人でやっていたのだけど他に触れる人がいない状況になっていたのでお手伝いから始めて、今は開発を分担できるレベルになったところ。Cast は情報が公式 docs くらいしかなく、それもあまり親切な記述ではないところもあって勘違いに苦しめられることが多々あった。

3つ目、チームメンバーの何人かと個人的に 1on1 のようなことをしている。きっかけは仕事の調子が悪そうなのだけど特にヘルプを求めてくるでもないメンバーがいたのでつついてみたところ、しっかり抱え込んでいたというところから。その場でいくらか話を聞いた感じすぐに解決できるようではなかったので、 1on1 の形を借りて定期的に相談を聞きつつ悩み事を整理してもらう場を設けて今まで続けており、当人曰く状況が好転したとのこと。今はもう1人新卒入社のメンバーとも 1on1 をしている。こちらは始めたばかり (きっかけの人は前いた時に一緒に働いてた) なのと、今年転職してからはじめましてのメンバーなので、まだお互いのキャラクターを掴むために雑談中心でやっている。最初に書いたように組織的なものではなく個人で勝手にやっていることなので、チームの中でも関わってない人もいる。なので、この活動がチームの関係性に影響を与える、端的にいえば派閥を形成することのないように注意はしているが、中々バランスが難しい。じゃあチームでの活動にすればいいじゃんという意見もあるだろうが、そういうピープルマネジメントをしたくないからウチでエンジニアやっとるんじゃという人もいるので...

引越し

去年の振り返りを読んだら2020年にやりたいことだった。実現できて良かったですね。

転職してすぐくらいから物件を探したり問い合わせをしたりして、実際に引越したのは11月という感じ。今年の半分以上引越しのことを考えていた気がする。フルリモートになって通勤を考慮しなくて良くなったので、部屋の広さの他に場所のために妥協していた楽器演奏ができる物件という条件で探して、最終的に都心からはやや離れたところにある24時間演奏可能な防音室つき物件に入居することができた。

楽器を弾くなら自宅でなくともカラオケやスタジオに行って弾くことも可能ではあるけど、これまでそうしようと思ってもできなかったことから私が楽器を触りたいと思ったときに触れる状態にすべきと考えたので、部屋の広さや設備を多少犠牲にすることは許容しようということになり、結果としてキッチンとリビングの設備が犠牲になった。

そんなこんなで楽器を再開することになったが、長年放置していた楽器のメンテをどうするかというのが直近の悩み事。

リモートワーク

働きすぎるのをどうにかしたい。オフィスで働いてた頃よりもモニタに張り付いてる時間が長い。

暮らし

元々外出は多くない方だったけど、輪にかけて外出をしなくなった。私は家に籠もりきりな生活でも特に問題はないと感じているのだが、展示会とか演奏会とかに行かなくなって文化の摂取量が減ったなあと思うことがあり、何かしらネガティブに働いているかも知れない。

運動量は減った。リングフィットアドベンチャー手に入れられなかった勢なので。

買ったもの

引越してからは色々家具家電を新調した、またする予定。

例えば仕事場デスクは FLEXISPOT のスタンディングデスクにした。

nashcft.hatenablog.com

この記事を書いた後にモニタとしてDELL の U3219Qを1枚、あとデスクマットを買うなどした。モニタはもう1枚あってもいいかもと思っているが、どっちかというとTV系端末の検証用モニタとして小さめのを買った方がいいかも知れないなという感じ。

あとは部屋が増えたり広くなったりしたので加湿器や空気清浄機も新しいものを買い足したり、食洗機を導入したりした。他にも色々買う予定のものがあるのだけど、思いの外引越しが遅くなってしまいすぐに年末になったので、大きいものなんかは年が明けてからでいいやということになった。

OSS活動

今年一番印象に残ってるのは Androidアプリ開発をやるようになってから初めて DroidKaigi のアプリに contribute したことか。

nashcft.hatenablog.com

あと Groupie に ViewBinding のサポートとその他いくつか小さなPRを送った。

nashcft.hatenablog.com

現在は転職して Groupie を使わなくなってしまったので repository を眺めるくらいしかしてないけど、あれからまた活動が止まって、息を吹き返したかと思ったらまたメンテナが替わり、そしてまた動きがあるようなないような感じで、先行きが不安な状態はまだしばらく続きそう。

github.com

また、Google Cast まわりを触っていたときに SDKktx library を作って公開した。

nashcft.hatenablog.com

上の記事を書いて以降 play-services-cast-tvAPIを触る機会があったのだけど、感想として拡張関数生やせるといえば生やせるが生やした方がいいかというというと考え込んでしまう、しかしいまのところ作れるところは作るスタンスになってるので気が向いたらやっておくか、という所感。個人的な印象で言うと Cast Connectの実装やってるところ Google自身以外にどれだけあるの? みたいなところがあるので、 play-services-cast-tv向けの開発はそんなにモチベーションが上がらない...

2021年にやりたいこと

2021年からもしばらくは自宅にいる時間が長くなると思うので、自宅でできる娯楽や活動を増やせるといいなと思っている。技術的な方面は、何か基礎的なところをしっかりやっていかんとなという所感。本は買うだけ買ってあるのでいい加減積んでるのを崩していきたい。

おわりに

有線のノイズキャンセリングヘッドホンが欲しい。おすすめ教えてください。

InstantTaskExecutorRule がやってること

$
0
0

社で何これって聞かれてした説明を見えるところに文章で残しておこうと思ったもの。

👀

使い方

class MyTest {

  @get:Ruleval instantTaskExecutorRule = InstantTaskExecutorRule()

  fun `test your component using AAC library`() {
    // LiveData とか Room とかが絡むテスト
  }
}

予備知識: @Rule, TestRule, TestWatcherについて

InstantTaskExecutorRuleの中身

小さいクラスなので実際のコードは上記の Code Search 等で見てもらうとして、やってることは以下のとおり:

  • テスト実行前に ArchTaskExecutordelegateを設定する
    • 設定した delegateisMainThreadは常に trueを返し、 executeOnDiskIOpostToMainThreadは与えられた Runnableをそのまま実行する
  • テスト終了時に ArchTaskExecutorに設定した delegateを破棄する

ArchTaskExecutor

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:arch/core/core-runtime/src/main/java/androidx/arch/core/executor/ArchTaskExecutor.java

ArchTaskExecutorは、非同期タスクの実行を担う singleton object で、 LiveDataLifecycleRegistry, RoomDatabaseで使われている。Main thread か IO thread でタスクを実行するためのAPI (postToMainThread, executeOnDiskIO) を持ち、またそれらでタスクを実行する Executorを取り出すこともできる。

ArchTaskExecutorにはもう一つ、今実行されている thread が main thread かを判定する isMainThreadという APIがある。これは main thread 上での実行を要求している API内部で assertion のために使われ、例えば LiveDataでは observe するときや setValueの中で呼ばれている。

タスクの実行部分は delegateされており、 InstantTaskExecutorRuleが行っているように setDelegateを使って外から設定することができる。ユーザが delegateを与えない場合は DefaultTaskExecutorが使用される。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:arch/core/core-runtime/src/main/java/androidx/arch/core/executor/DefaultTaskExecutor.java

DefaultTaskExecutorpostToMainThreadでは、main looper に対する Handlerを使ってタスクを main thread に post している。 isMainThreadは、 main looper に紐ついてる thread と current thread を比較している。

JUnit local test と main looper

そもそもなんで使う必要があるのかというと、Androidアプリとしてコードを実行する時と local test を実行する時の環境の差が関係している。Androidアプリとして実行される際は、大雑把に説明すると、アプリのプロセスが起動した後まず ActivityThreadで main looper の準備と実行が行われ*1、それから諸々の準備ができた後に我々の書いたアプリケーションコードが動く。しかし local test は JUnit test として実行されるので、Androidアプリに関わるセットアップの類は実行されない。これがどう関係するかというと、local test 実行時には main looper のセットアップが行われず Looper#getMainLoopernullのままとなるので、それに依存する Handlerや、 main looper をもとに "Androidアプリにおける main thread"を判定する処理が機能しなくなってしまう。

class MyTest {

  fun test() {
    val mainLooper = Looper.getMainLooper()  // nullval livedata = MutableLiveData<Int>()
    livedata.observeForever { println("onChanged") }
    // java.lang.NullPointerException//     at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread//     at androidx.arch.core.executor.ArchTaskExecutor.isMainThread//     at androidx.lifecycle.LiveData.assertMainThread//     at androidx.lifecycle.LiveData.observeForever
  }
}

この問題に対しては 1) main looper をテスト実行前に用意する、もしくはあるように偽装する、2) LooperHandlerに触れない形に変える、という解決方法を考えることができ、 2) を実現する手段として InstantTaskExecutorRuleが用意されている、ということになる。

Robolectric による解決と比較

ところで Robolectricを使っても main looper の問題を解決することは可能である。Robolectric はテスト実行時に Android framework のコードを実行したり APIを偽装してくれたりする。つまり上で挙げた 1) のアプローチを取ることになる。今回のケースでは、テストケース実行時には Looperに main looper が準備された状態になっているため、 ArchTaskExecutorDefaultTaskExecutorを使っても NPE でクラッシュすることはなくなる。

@RunWith(AndroidJUnit4::class)
class MyTestWithRobolectric {

  fun `test with robolectric`() {
    val mainLooper = Looper.getMainLooper()  // non-null valueval livedata = MutableLiveData<Int>()
    livedata.observeForever { println("onChanged") } // observe できる
  }
}

ただし Robolectric による Android framework 部分のセットアップには少なくとも数秒以上の時間がかかるので、「ユニットテスト」として評価した場合は有意に遅くなる *2。あと、非同期処理の待ち合わせについて何かやってくれるということはないので、例えば postValueしている箇所がある場合は特別な考慮が必要になる。

@RunWith(AndroidJUnit4::class)
class MyTestWithRobolectric {

  fun `test with robolectric`() {
    val livedata = MutableLiveData<Int>()
    var count = 0
    
    livedata.observeForever {
      println("onChanged: $it")
      count++
    }

    livedata.value = 1// テスト実行中に observer に通知が届く = 処理される
    livedata.postValue(2)  // 通知が届かない

    assertThat(count).isEquelTo(2) // Expected:2, Actual:1
  }
}

InstantTaskExecutorRuleArchTaskExecutordelegateを差し替えるだけなので非同期タスク処理の関わる部分がここのみの場合のみ有効だが、その分テストの実行時間への影響は小さい。また ArchTaskExecutorを通して行われる非同期処理もただ同期的に実行するようになるので、待ち合わせについて考慮する必要がなくなる。

class MyTestWithInstantTaskExecutorRule {

  @get:Ruleval instantTaskExecutorRule = InstantTaskExecutorRule()

  fun `test with InstantTaskExecutorRule`() {
    val livedata = MutableLiveData<Int>()
    var count = 0
    
    livedata.observeForever {
      println("onChanged: $it")
      count++
    }

    livedata.value = 1// ArchTaskExecutor#postToMainThead の振る舞いが同期化されるので、// postValue もテスト実行時に observer に通知が届くようになる
    livedata.postValue(2)

    assertThat(count).isEquelTo(2) // passed
  }
}

使い分けとしては, LiveData や Room しか関わらないような local test については InstantTaskExecutorRuleだけ使って、他にも Android framework への依存があるようなコンポーネントに対する local test では Robolectric も使う、みたいな感じで良いと思う。

まとめ

  • AACコンポーネントのいくつかはスレッド確認や非同期実行を行う際に ArchTaskExecutorを使用している
  • ArchTaskExecutorは振る舞いの実体を delegateしており、デフォルトでは DefaultTaskExecutorを使用している
  • DefaultTaskExecutorは main thread の確認に Looper#getMainLooperを参照している
  • Local test は Android framework に関わるセットアップを行ってくれないため、 main looper の準備も行われない
  • InstantTaskExecutorRuleArchTaskExecutordelegateを差し替えて、実行 thread の確認を何も見ずに答えたり非同期実行を担う箇所を同期的に処理したりするように挙動を変更することで、local test でも AAC関連の非同期処理や main thread 制約の関わる部分をよしなに動くようにしてくれる

*1:この辺は『Android を支える技術 <I>』の 1.3.2 あたりを読むと良い

*2:Robolectric が「高速」と謳われる文脈は実機や emulator を用いたデバイステストとの比較で、apk をビルドしてデバイスにインストールして実行するのと比べたら当然高速

Software Design 2021年3月号

$
0
0

gihyo.jp

第1特集「Javaでもう一度学び直すオブジェクト指向プログラミング」を読むために購入。

Javaに限定するならば、自分だったらこちらより "Effective Java"を勧めるかな、導入が丁寧で好印象だったけど本編が荒っぽくて残念という感想。1冊の本と雑誌の1特集を比べるのはフェアではないとは思うけど、紙面が限られているという都合以上に記述や主張に考慮が行き届いていないように感じられ、これからオブジェクト指向プログラミングについて学びたい!という人には視野を狭めてしまう懸念があるなというのが理由。自分の中に解釈があって実践をしている人にとっては議論のネタとしていいかもしれない。

いや Effective Javaオブジェクト指向プログラミングの本ではないじゃん、って言われると思うけど、この特集のトピックは interface と継承、合成なので、それだったら Effective Javaで十分カバーできてるなーって思ったので。

www.maruzen-publishing.co.jp

実装の詳細をテストすることについての所感

$
0
0

下書きにずっと残ってたので供養。本当は色々書くつもりだったけど量的にも内容的にも一生まとまりそうになかったから自分の主張分だけ少し体裁を整えて出すことにした。

実装の詳細のテストは書いてもいい

ただし成果物をコミットするまでには消しておいた方がいい。

テストの目的や内容によって寿命が異なることをを意識しよう

  • 寿命が短い
    • 実装している自分のためのテスト
      • 意図通りに動いている?
      • 自分の実装に自信を持つためのテスト
      • 確認が取れ次第寿命を迎える
  • 寿命が (比較的) 長い
    • 機能単位の振る舞い (仕様) に関して記述されたテスト
      • 要求に対してこの機能が達成したいことを伝える
      • よりユーザとの距離が近いものほど寿命が長くなることが期待できそう
        • e.g.) モジュール >クラス >関数
        • 逆にユーザに近いもののテストが壊れやすかったら詳細のテストが紛れているサインかも
        • 単に仕様が生煮えなだけかもしれない
      • 仕様が変わった、機能がいらなくなったら寿命を迎える

寿命に応じて適宜テストを削除しよう

残されたテストはそれに意図があると受け取られて消されにくい。そしてその後の実装や開発活動に影響を与える、場合によっては無意味な足枷になりうる (関連: "テストコードの慣性の力")。

既に書いた、もしくはこれから書くテストの目的から寿命を把握して、寿命を迎えたらきちんと消してあげる。テストが書かれてから消えるまでの過程がコミットログに残る形である必要はない。


Jetpack ViewModel はいつ clear されるか

$
0
0

tl;dr

onPause
↓
onStop
↓
(onDestroyView)
↓
onCleared
↓
onDestroy

Jetpack ViewModel のドキュメントには Activity と紐つけた際の owner の lifecycle event と ViewModel の生存期間の関係についての図が載っている。

f:id:nashcft:20210307164204p:plain
https://developer.android.com/topic/libraries/architecture/viewmodel#lifecycleより引用

この図では ViewModel#onClearedの実行は Activity#onDestroy実行より後のタイミングで起こるように描かれており、そのイメージから ViewModel 登場以来ずっと ViewModel#onClearedonDestroyより後に実行されるものと思っていたが、現在のコードを読んでみると実際はそうではなく onDestroy()の前に実行されることに気がついた。この記事では ViewModel破棄の実際の流れを追っていく。適当な ViewModel の onCleared()が呼ばれる時の stacktrace を見ればわかる話なので、それで十分という人はここで読むのをやめて実際にそちらを見にいくと良い。

この記事の内容は以下の version で動作を確認している:

  • androidx.activity: 1.0.0-alpha01および 1.2.0
  • androidx.fragment: 1.1.0-alpha01および 1.3.0

確認に使ったコードの repository:

github.com

ViewModel が破棄される仕組み

ViewModel は生成時に ViewModelStoreに登録される。この ViewModelStoreは ViewModel を HashMapで保持しており、 ViewModelStore#clearが呼ばれた時に保持している ViewModel の clear()を実行し、保持している ViewModel を破棄する。 ViewModel#onClearedViewModel#clear内で実行される。保持している ViewModel の clear が終わったら、 map を clear して参照を切る。

https://cs.android.com/androidx/platform/frameworks/support/+/2e1f4a8ef6eb4252735ed377398bc56f310406c0:lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModelStore.java

https://cs.android.com/androidx/platform/frameworks/support/+/636466bf953fa67e04ba56a6f1c04a8a14220595:lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java

ViewModelStore#clearが呼ばれる流れ: Activity 編

Activity は ViewModelStore#clearが呼ばれている箇所から遡って辿るとわかりやすいのでそのように説明する。

Activity が持つ ViewModelStoreclear()は、 androidx.activity.ComponentActivity (以下 ComponentActivity*1 ) の constructor で自身の Lifecycle に対して登録している LifecycleEventObserverの中の1つで呼ばれている。

public ComponentActivity() {
        // ...
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Overridepublicvoid onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    // Clear out the available context
                    mContextAwareHelper.clearAvailableContext();
                    // And clear the ViewModelStoreif (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
        // ...
    }

https://cs.android.com/androidx/platform/frameworks/support/+/6f45d365ff22e50c82467ce0d6208433d5ec19d1:activity/activity/src/main/java/androidx/activity/ComponentActivity.java;l=238-251

上に該当する箇所を抜粋したが、 Lifecycle.Event.ON_DESTROYが通知された際、configuration change によるものではない場合に ViewModelStore#clearが実行されることがわかる。

この ComponentActivity (を継承した Activity) の Lifecycle に対して lifecycle event を送るのは誰かというと、 ReportFragmentという event 通知用の内部 class である。

https://cs.android.com/androidx/platform/frameworks/support/+/ec4a052c29df9691bd35a936541e6ab46b97514b:lifecycle/lifecycle-runtime/src/main/java/androidx/lifecycle/ReportFragment.java

ReportFragmentは、static method の injectIfNeededIn()で渡した Activity の、 Android Framework の方の FragmentManagerにこの Fragment を add することで、 Activity の Lifecycle*2に対して lifecycle event を通知する機能を持っている。実際の通知の仕組みは API level 29 以上とそれ未満とで異なり、API level 29 以上では通知機能を実装した Application.ActivityLifecycleCallbacksを Activity に登録して、 29 未満では ReportFragmentの各 lifecycle の callback method を通して通知を実現している。

publicstaticvoid injectIfNeededIn(Activity activity) {
        if (Build.VERSION.SDK_INT >= 29) {
            // On API 29+, we can register for the correct Lifecycle callbacks directly
            LifecycleCallbacks.registerIn(activity);
        }
        // Prior to API 29 and to maintain compatibility with older versions of// ProcessLifecycleOwner (which may not be updated when lifecycle-runtime is updated and// need to support activities that don't extend from FragmentActivity from support lib),// use a framework fragment to get the correct timing of Lifecycle events
        android.app.FragmentManager manager = activity.getFragmentManager();
        if (manager.findFragmentByTag(REPORT_FRAGMENT_TAG) == null) {
            manager.beginTransaction().add(new ReportFragment(), REPORT_FRAGMENT_TAG).commit();
            // Hopefully, we are the first to make a transaction.
            manager.executePendingTransactions();
        }
    }

    // ...@RequiresApi(29)staticclass LifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
        // ...
    }

この ReportFragmentLifecycle.Event.ON_DESTROYを Activity に通知するのは ActivityLifecycleCallbacks#onActivityPreDestroyedまたは ReportFragment#onDestroyである。片方はそのままな名前をしているのでわかったようなものだが、これらが実行されるのは android.app.ActivityperformDestroy中、前者は dispatchActivityPreDestroyed()で、後者は mFragments.dispatchDestroy()の先であり、 onDestroyの前となる。

finalvoid performDestroy() {
        dispatchActivityPreDestroyed();  // 筆者註: API >= 29 の場合はここで ViewModel が破棄される
        mDestroyed = true;
        mWindow.destroy();
        mFragments.dispatchDestroy();  // 筆者註: API < 29 の場合はここで ViewModel が破棄される
        onDestroy();
        EventLogTags.writeWmOnDestroyCalled(mIdent, getComponentName().getClassName(),
                "performDestroy");
        mFragments.doLoaderDestroy();
        if (mVoiceInteractor != null) {
            mVoiceInteractor.detachActivity();
        }
        dispatchActivityPostDestroyed();
    }

https://cs.android.com/android/_/android/platform/frameworks/base/+/eff0b0673c1604477b815cf1bcf001b1df56ef75:core/java/android/app/Activity.java;l=8240-8253

ComponentActivityonCreateReportFragment#injectIfNeededInに自身を渡しており、それによって上記のように Lifecycle に対して event 通知を受け、 finish の際に ON_DESTROYを受け取ったところで ViewModel を破棄している、という流れになる。

@Overrideprotectedvoid onCreate(@Nullable Bundle savedInstanceState) {
        // ...super.onCreate(savedInstanceState);
        mActivityResultRegistry.onRestoreInstanceState(savedInstanceState);
        ReportFragment.injectIfNeededIn(this);
        // ...
    }

https://cs.android.com/androidx/platform/frameworks/support/+/6f45d365ff22e50c82467ce0d6208433d5ec19d1:activity/activity/src/main/java/androidx/activity/ComponentActivity.java;l=288-300

ViewModelStore#clearが呼ばれる流れ: Fragment 編

androidx.fragment.app.Fragmentの場合は起点はわかりやすいが辿る経路が長いのと実際に呼ばれてる場所がちょっと不思議なのでとりあえずスタート地点から。

Fragment に紐つく ViewModelStoreclear()が呼ばれるきっかけになるのは FragmentActivity#onDestroy, つまり Fragment#onDestroyが呼ばれるのと同じである。そこから FragmentController ->FragmentManager ->SpecialEffectsController ->FragmentStateManagerと進んでいって、この FragmentStateManager#moveToExpectedStateFragmentStateManager#destroyに入る。この経路自体は特に重要ではないので適当に端折ったが、実際に追ってみたい場合は stacktrace を出してそれに沿って読んでみると良い。

この FragmentStateManager#destroyの中、その先で Fragment#onDestroyが実行される mFragment.performDestroy()の直前にある mFragmentStore.getNonConfig().clearNonConfigState(mFragment), ここで Fragment に紐つく ViewModelStoreの clear が行われる。

void destroy() {
        // ...boolean beingRemoved = mFragment.mRemoving && !mFragment.isInBackStack();
        boolean shouldDestroy = beingRemoved
                || mFragmentStore.getNonConfig().shouldDestroy(mFragment);
        if (shouldDestroy) {
            // ...if (beingRemoved || shouldClear) {
                mFragmentStore.getNonConfig().clearNonConfigState(mFragment);
            }
            mFragment.performDestroy();
            // ...
        } else {
            // ...
        }
    }

https://cs.android.com/androidx/platform/frameworks/support/+/cfc31d1abe7bd6c064a8780add0033853b1a3c91:fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java;l=747-750

ここで NonConfig と呼ばれる FragmentManagerViewModelは、端的に言うと Activity の ViewModelStoreに登録される ViewModel で、それぞれの Fragment に対応する ViewModelStoreの管理や Activity の再生成時に retain される Fragment の保持といった役割を持っている。 clearNonConfigState()では、与えられた Fragment に対応する ViewModelStoreと子の NonConfig の clear および管理下からの削除を行っている。

void clearNonConfigState(@NonNull Fragment f) {
        if (FragmentManager.isLoggingEnabled(Log.DEBUG)) {
            Log.d(TAG, "Clearing non-config state for " + f);
        }
        // Clear and remove the Fragment's child non config state
        FragmentManagerViewModel childNonConfig = mChildNonConfigs.get(f.mWho);
        if (childNonConfig != null) {
            childNonConfig.onCleared();
            mChildNonConfigs.remove(f.mWho);
        }
        // Clear and remove the Fragment's ViewModelStore
        ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
        if (viewModelStore != null) {
            viewModelStore.clear();
            mViewModelStores.remove(f.mWho);
        }
    }

https://cs.android.com/androidx/platform/frameworks/support/+/a48dae920b797f8e4c14aa9c74277fdf85137e76:fragment/fragment/src/main/java/androidx/fragment/app/FragmentManagerViewModel.java;l=186-202

Fragment まわりのコードはややこしくて読むのに苦労するが、とりあえず FragmentStateManager#destroyの中で Fragment#performDestroy (= Fragment#onDestroyの実行) の直前に紐ついてる ViewModel の clear が行われていることがわかった。

雑多な話題

なんでドキュメントは嘘ついてるの?

実装当初は強ち嘘というわけではなかった。もともと ViewModelStore#clearFragmentActivity, Fragmentそれぞれの onDestroyの中で実行されていて、super#onDestroyを override した method の最後で呼んだ場合は呼ばれる順番として onDestroy -> onClearedと捉えることもできた。

いつから現在の挙動に変わったの?

動作確認している version で察しがつく人もいると思うが、 release version では AndroidX として一番最初である activity:1.0.0-alpha01 / fragment:1.1.0-alpha01からである。 Commit のレベルでは 2018-10-23 に commit された以下の2つ:

これを挙動の変更と言えるかは難しくて、 Activity のCLにつけられたコメントを見ると、 super.onDestroy()を呼ぶタイミングがアプリ開発者に委ねられてしまっていることを問題視していて、現在のような挙動を常にすることを保証したかったように読み取れる。

Through discussions with adamp@, we wanted to make a firm decision on exactly when this will run as the final LifecycleObserver going out. onDestroy() does not offer the same guarantee since developers can call it at any point in their onDestroy() method.

実際 onPause ~ onDestroyの super method を最初に呼ぶ流派と最後に呼ぶ流派があるようだし*3、またうっかり super.onDestroy()を呼び忘れることによって clear されないということも起こりうるので、フレームワークの仕組みとしては現在の形の方が正しいのかもしれない。そうするとやっぱりドキュメントの図は嘘をついてることになってしまうので直すべきでは? となるが...

書いてて気づいたが Fragment の destroy と Fragment に紐つく ViewModel の clear はまだ FragmentActivity#onDestroyに依存しているので、 super.onDestroy()の呼び忘れで実行されないということがありうる。呼ばれる順序は常に同じにすることができたけど、そもそも呼ばれないという問題はまだ解消できていないようだ。

viewModelScopeについて

viewModelScopeViewModel#clear実行時、 ViewModel#onClearedが呼ばれる前に cancel される。

https://cs.android.com/androidx/platform/frameworks/support/+/20d4085eedeb1f83964a802cf6286bfbdba57498:lifecycle/lifecycle-viewmodel-ktx/src/main/java/androidx/lifecycle/ViewModel.kt

FragmentActivityLifecycleRegistryには2回 ON_DESTROYが通知されるけど

上で説明した ComponentActivity由来の経路と FragmentActivity#onDestroyで実行されている mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)で計2回 ON_DESTROYLifecycleRegistryに送られている。

1回目の event 通知時の ViewModelStore#clearで保持していた ViewModel は全て切り離されるので ViewModel#onClearedが2回呼ばれるようなことはない。

まとめ

Activity (API>=29)

ViewModel cleared at com.github.nashcft.app.ActivityViewModel.onCleared(ActivityViewModel.kt:23)
        at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
        at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
        at androidx.activity.ComponentActivity$4.onStateChanged(ComponentActivity.java:261)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
        at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.java:284)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:302)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
        at androidx.lifecycle.ReportFragment$LifecycleCallbacks.onActivityPreDestroyed(ReportFragment.java:224)
        at android.app.Activity.dispatchActivityPreDestroyed(Activity.java:1498)
        at android.app.Activity.performDestroy(Activity.java:8241)
        at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1344)
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5096)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5140)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:44)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

Activity (API< 29)

ViewModel cleared at com.github.nashcft.app.ActivityViewModel.onCleared(ActivityViewModel.kt:23)
        at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
        at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
        at androidx.activity.ComponentActivity$4.onStateChanged(ComponentActivity.java:261)
        at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.java:354)
        at androidx.lifecycle.LifecycleRegistry.backwardPass(LifecycleRegistry.java:284)
        at androidx.lifecycle.LifecycleRegistry.sync(LifecycleRegistry.java:302)
        at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.java:148)
        at androidx.lifecycle.LifecycleRegistry.handleLifecycleEvent(LifecycleRegistry.java:134)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:68)
        at androidx.lifecycle.ReportFragment.dispatch(ReportFragment.java:144)
        at androidx.lifecycle.ReportFragment.onDestroy(ReportFragment.java:134)
        at android.app.Fragment.performDestroy(Fragment.java:2782)
        at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1451)
        at android.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManager.java:1576)
        at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1637)
        at android.app.FragmentManagerImpl.dispatchMoveToState(FragmentManager.java:3046)
        at android.app.FragmentManagerImpl.dispatchDestroy(FragmentManager.java:3026)
        at android.app.FragmentController.dispatchDestroy(FragmentController.java:248)
        at android.app.Activity.performDestroy(Activity.java:7394)
        at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1306)
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:4443)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:4476)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:39)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:145)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:70)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

Fragment

ViewModel cleared at com.github.nashcft.app.FragmentViewModel.onCleared(FragmentViewModel.kt:23)
        at androidx.lifecycle.ViewModel.clear(ViewModel.java:138)
        at androidx.lifecycle.ViewModelStore.clear(ViewModelStore.java:62)
        at androidx.fragment.app.FragmentManagerViewModel.clearNonConfigState(FragmentManagerViewModel.java:199)
        at androidx.fragment.app.FragmentStateManager.destroy(FragmentStateManager.java:769)
        at androidx.fragment.app.FragmentStateManager.moveToExpectedState(FragmentStateManager.java:350)
        at androidx.fragment.app.SpecialEffectsController$FragmentStateManagerOperation.complete(SpecialEffectsController.java:742)
        at androidx.fragment.app.SpecialEffectsController$Operation.cancel(SpecialEffectsController.java:594)
        at androidx.fragment.app.SpecialEffectsController.forceCompleteAllOperations(SpecialEffectsController.java:329)
        at androidx.fragment.app.FragmentManager.dispatchStateChange(FragmentManager.java:3116)
        at androidx.fragment.app.FragmentManager.dispatchDestroy(FragmentManager.java:3091)
        at androidx.fragment.app.FragmentController.dispatchDestroy(FragmentController.java:334)
        at androidx.fragment.app.FragmentActivity.onDestroy(FragmentActivity.java:322)
        at com.github.nashcft.app.MyFragmentActivity.onDestroy(MyFragmentActivity.kt:43)
        at android.app.Activity.performDestroy(Activity.java:8245)
        at android.app.Instrumentation.callActivityOnDestroy(Instrumentation.java:1344)
        at android.app.ActivityThread.performDestroyActivity(ActivityThread.java:5096)
        at android.app.ActivityThread.handleDestroyActivity(ActivityThread.java:5140)
        at android.app.servertransaction.DestroyActivityItem.execute(DestroyActivityItem.java:44)
        at android.app.servertransaction.TransactionExecutor.executeLifecycleState(TransactionExecutor.java:176)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:97)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

*1:余談だが androidx.core にも ComponentActivity が存在し、 androidx.activity.ComponentActivity の直接の親 class となっている

*2:実際には getLifecycle() で取得できる Lifecycle が LifecycleRegistry である必要がある

*3:要出典

Android project を Gradle 7.0 にしたら JitPack で publish できなくなった

$
0
0

tl;dr

発見

私が作ってる cast-sdk-ktxは JitPack を使ってるのだけど、数日前の Cast SDKのバージョン更新に対応して、ついでに色々依存をアップデートしたのをリリースしようとしたら publish でこけてしまった。
ローカルで build する分には問題なかったので何でだろーって build log を見たところ以下のような log が出てた:

> Configure project :
Gradle version Gradle 7.0

FAILURE: Build failed with an exception.

* Where:
Script '/script/maven-plugin.gradle' line: 2

* What went wrong:
A problem occurred evaluating script.
> Failed to apply plugin 'com.github.dcendents.android-maven'.
   > Could not create plugin of type 'AndroidMavenPlugin'.
      > Could not generate a decorated class for type AndroidMavenPlugin.
         > org/gradle/api/publication/maven/internal/MavenPomMetaInfoProvider

com.github.dcendents.android-mavenが原因とのこと。

調査

JitPack では publish のための build の際、project の gradle tasks を調べ、 publish に必要な task が存在しなかった場合にそれを追加するために plugin を導入する。
例として cast-sdk-ktx:0.1.0の build log を見てみると、以下のような出力が見られる:

...

Getting tasks: ./gradlew tasks --all
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8 -Dhttps.protocols=TLSv1.2
Tasks: 

WARNING:
Gradle 'install' task not found. Please add the 'maven' or 'android-maven' plugin.
See the documentation and examples: https://jitpack.io/docs/

Adding android plugin
Adding maven plugin
Found android library build file in cast-ktx
Found android library build file in cast-framework-ktx
Found android library build file in cast-tv-ktx
Running: ./gradlew clean -Pgroup=com.github.nashcft -Pversion=0.1.0 install
...

https://jitpack.io/com/github/nashcft/cast-sdk-ktx/0.1.0/build.log

上の抜粋では install task が見つからないことを理由に plugin を導入すると言われている。この際に /script/maven-plugin.gradleの apply が追記されて、その中に com.github.dcendents.android-mavenを使った publication の設定が記述されているのだろう。

今回はこの plugin の apply 中にエラーが発生しているので、原因は android-maven plugin 中の実装と考えられる。Build script の実行時に plugin のエラーが起こったという状況なので関連するこちら側の変更は Gradle のバージョンだろう。Build が成功している 0.1.0と今回エラーが発生した commit では Gradle が 6.8.3 から 7.0 に上がっている。

ところでこの com.github.dcendents.android-mavenだが、既に役割を終えているためにメンテナンスが止まっており、 repository も archive されている。

github.com

というわけで今回の問題が修正されることはないし、じゃあこれ以上は詮無しということで原因を追いかけるのも終わり。きっと "Could not generate a decorated class for type"でググれば出てくるだろう。

対処

上述のように publication のための plugin が project に入ってないと com.github.dcendents.android-mavenを使われてしまうので、project に publication のための設定を追加することでこの問題を回避できる。
AndroidMaven Plugin の README でも言及があるが、 AGP 3.6.0 以上では Gradle の Maven Publish Pluginが使えるので、それを apply して設定を追記すれば publication ができるようになる。

developer.android.com

JitPack の GitHub organization にある Androidの sample repo を見た感じ最低限の設定だけ書けば動きそう。

https://github.com/jitpack/android-example/blob/19d4ad07071b410dce8bccde0727fd159327db93/library/build.gradle

というわけで cast-sdk-ktxにも Maven Publish Plugin を導入する pull request を作った。Publication の設定にどんなのがあるか色々眺めながら書いてたので JitPack の example より記述量が多くなっている。

github.com

これを書くことになると JitPack のいいところだったお手軽 publish の度合いが下がるので、もうちょっと頑張って Maven Central に移行してしまってもいいのでは...? と思わないこともない。

JitPack で AGP 7.0 以上の Android project を扱う時の注意点

$
0
0

さっき投稿した記事でエラーの原因を調査していたときに JitPack の document を読んでて気づいたのだけど、 JitPack の build 環境では JDKはデフォルトで 1.8 となっているようだ。

https://jitpack.io/docs/ANDROID/

Builds are run with Java 8 by default but can be configured using a jitpack.yml file.

Android Gradle Plugin は Arctic Fox Canary 9 から JDK 11 を要求するようになったので、それ以降のバージョンの AGPを使っている場合、当面は上の引用にあるように jitpack.ymlを repository に追加してそちらで build 環境設定をカスタマイズする必要がある。

androidstudio.googleblog.com

JDK 11 required to run AGP 7.0

When using Android Gradle plugin 7.0 to build your app, JDK 11 is now required to run Gradle. Android Studio Arctic Fox bundles JDK 11 and configures Gradle to use it by default, which means that mostAndroid Studio users do not need to make any configuration changes to their projects.

jitpack.ymlによるカスタマイズは以下のページに書かれている:

https://jitpack.io/docs/BUILDING/#custom-commands

JDKのバージョンだけ変えるのであれば、以下の内容の jitpack.ymlを repository の root に追加すればよさそう。

jdk:- openjdk11

特定のバージョンの JDKを使いたい時は SDKMAN使って install してねとも書いてあるけど sdk command 使えない雰囲気があって謎。

AGP 4.0.0 以降で発生する local aar 起因のビルドエラーについて

$
0
0

起こってること

AGP 4.0.0 から local aar に依存している library module をビルドすると失敗するようになった。 Local aar というのは libs/以下に .aar が置いてあって以下のように依存してるみたいなやつ。

dependendencies {
  implementation(files("libs/my-aar.aar"))
}

このときの error message は以下のような感じ:

Direct local .aar file dependencies are not supported when building an AAR. The resulting AAR would be broken because the classes and Android resources from any local .aar file dependencies would not be packaged in the resulting AAR. Previous versions of the Android Gradle Plugin produce broken AARs in this case too (despite not throwing this error). The following direct local .aar file dependencies of the :[module name] project caused this error: [path to .aar file]

関係しそうな commit は以下:

この時点で影響したのは library module のビルドだけで*1、直接ビルドする必要がなければ = それに依存する aab/apk だけ成果物として欲しければ、 application module を指定してビルドするとなんでか成功するので*2そのような回避方法もある。

そのあと AGP 7.0.0 で bundle<Variant>LocalLintAarという task が追加されて、これのために lint task でも同様の build error が発生するようになった。

関連 commit:

Lint に関しては特に逃げ道がなさそうだったのできちんと対応するしかなさそう。 Local aar を local module として使うようにすると解決する。

Local aar を local module として扱う

まず module 用の directory を作って以下のファイルを置く:

  • build.gradle
  • 使いたい aar ファイル

build.gradleの中身は以下:

configurations.maybeCreate("default")
artifacts.add("default", file("my-aar.aar"))

最終的な directory の中身はこんな感じ:

my-aar-module/
+-- build.gradle
+-- my-aar.aar

あとは他の local module と同じように settings.gradleで include して依存元の記述を書き換えたらOK。

dependencies {
-  implementation(files("libs/my-aar.aar"))+  implementation(project("my-aar-module"))
}

終わりに

公式からアナウンスあるか軽く探した感じでは見つけられなかったのだけど何かあったっけ?

調べてた時のメモ:

scrapbox.io

*1:application module が local aar に依存している場合は問題なくビルドできる

*2:理由は調べてない

2021年振り返り

$
0
0

過去の

先に書くことがはっきりしていた買ったものについて書いていたらめちゃくちゃ長くなったので他はざっくり

仕事

とにかくモチベーションを保つのに難儀した1年という印象だけが残っている。

暮らし

夏くらいまでは引っ越し後の生活の基盤を整えるための活動と仕事で忙しくしてて、そのために人生の進捗は無いのにまるで人生が充足しているかのような錯覚と闘う日々が続いていたように思う。秋ぐらいからは少しずつ改善しつつあると感じているが、しかし一度止まってしまった人生の進捗を再び動かすのは難しい。

あと『劇場版 少女☆歌劇 レヴュースタァライト』が公開されてからはひたすらそれを観に行ってた。

技術的なあれそれ

OSS活動については cast-sdk-ktxを細々メンテしている以外は特に。それ以外だと https://cs.android.com/で読めるコードや kotlinx.coroutinesなど OSSのコードを読む時間が増えた。「自分の使っている道具については仕組みや背景をできるだけ理解して自在に扱えるようになりたい」という考えを持っているので、そのための以前より踏み込んだ活動ができるようになったのは素直に喜ぶべきか。

買ったもの

今年は色々買ったなって印象、実際色々買ってる。

Sennheiser MOMENTUM 3 Wireless

ja-jp.sennheiser.com

去年の振り返りでNCヘッドホン欲しいって書いた後、年明けてすぐに購入。無線でも有線でも使えるものが欲しかった。NCヘッドホンとしては満足しているけど、割と重いのであまり長い間つけていられないのがつらいところ。なおこれがまともに運用され始めるのは12月に入ってからだった。

リカバリーシューズ

製品としては ->https://www.amazon.co.jp/gp/product/B086T4N1B3/

引っ越し時にデスクマットとして買ったジョイントマットがダメになって普通のマットに替えたことでクッション的なものが無くなったので購入。元々はスタンディングデスクマットを買おうと考えていたけど、昇降デスクの上げ下げをする毎に引っ張り出したり退けたりするのが面倒くさいという結論になって、クッション性のあるサンダルにしようということでこれになった。

Panasonic衣類乾燥除湿機

panasonic.jp

前に住んでた部屋は浴室乾燥機があったので梅雨時はそこで洗濯物を乾かしていたけど今の部屋は付いてないので買った。部屋干しスペースの都合で背の低いのが欲しかった。

Fitbit Charge 5

www.fitbit.com

Activity tracker 欲しいねーって話をしていた時にちょうど発売されるところだったので購入。今はすっかり fitbit の奴隷となって毎日せっせと運動している。
他に昔から睡眠の質が低そうと人に言われていたのが綺麗に可視化されてハァンとなった。具体的には深い睡眠からREM睡眠に切り替わるところで突き抜けて覚醒状態まで行ってしまうので入眠から起床までの時間に対して実際に眠っている判定になる時間がそんなに多くないという状態だそう。早く寝てもたくさん寝てもあまり改善される気配がない...

オムロン体重体組成計 カラダスキャン

www.healthcare.omron.co.jp

実家を出て以来ずーっと体重計を持っていなくて健康診断の時くらいしか測ってなかったので、 Fitbit を買った時についでに買うかということで買った。体脂肪率とかは割といい加減な感じなので体重以外の値は参考程度に見てる。

折りたたみイス

学校とかにあるパイプ椅子みたいなやつ。楽器練習用に買った。

Herman Miller Embody Chair

www.hermanmiller.com

Ergohuman Enjoy から乗り換え。前々から欲しかったのをリモートワークも定着したし引っ越しも落ち着いたしそろそろいいでしょってことで購入。

ニトリパーソナルチェア

www.nitori-net.jp

くつろぐ用のイスが欲しくない? となり購入。

ダイニングチェア

kare.co.jp

二人暮らししてるのにまともなダイニングチェアが一個だけ (もう一個はやっすいキッチンチェア) っていう状況だったのでいい加減どうにかしましょうとなり購入。今年はやけにイスを買っているな...

Shure SRH1840

www.shure.com

MOMENTUM は音楽を聴く用としては音が籠っててダルく、他に持ってたのはパッド類がダメになってて使えないみたいな状況でオーディオ環境がずっと貧相だったのだけど、12月に入って突然環境整えたい!!!という気持ちが高まって購入したものその1。もともとモニター向けのヘッドホンが好きで、これはめっちゃ「音が聴こえる...」感があって最高。

Topping DX3Pro+ DAC+Headphone amp

www.tpdz.net

オーディオ環境整備するぞの気持ちで購入したものその2。はじめての DAC/ヘッドホンアンプ。インピーダンスが 50Ω を超えるヘッドホンを買うのは初めてだったのでこのくらいだと流石にアンプ必要になるかなと思い探し始めて見事に沼ったが、この時点で突然発生した出費の総額的に突き抜けたことはできない...! みたいな理性によりこれになった。Twitterで follow してる人の発言を中心に製品評価を見てたので、もし理性が飛んでたら RME ADI-2 DAC fs を買っていたことでしょう。ところで SRH1840 は思ったより音量を取ることができたのでアンプがないとダメってことはなかったのだった。せっかくだから 250Ω とかあるヘッドホン買おうかな。

RME ADI-2 DAC fs はこの記事を書いてる時点で14万くらいで買える: https://www.soundhouse.co.jp/en/products/detail/item/272248/

Shure MV7 & Blue Microphones Compass

www.shure.com

www.bluemic.com

これまでミーティングの時は以前買ったてきとうなヘッドセットを使っていたのだけど、やっぱり MOMENTUM を使いたいよなってなって、そうするとマイクが別途必要になるなあということで購入。元々はもうちょっと安く済ませようと思っていたのだけど上の2つを買った直後で財布の紐が緩くなってたのと自分の状況と似た人に薦められるものとして MV7 を挙げているのがあちこちで見られたのでマイクはとりあえず MV7 (高い) に。その後マイクは高いのにするからアームは安く抑えたいなーと amazonで探してたら compass (高い) と MV7 がセットで売られているのを見つけてしまい何かもうそういうことなんだなと納得してしまったのでこの組み合わせで購入した。現在は基本 MV7 & MOMENTUM を使って、長時間ミーティングが続くようなら以前のヘッドセットに切り替えるという運用をしている。

USBハブ

www.sanwa.co.jp

普段 laptop stand に立てかけているのでハブ本体とケーブルが分離できるものが欲しかった。別途 1m くらいのUSBケーブルを買って繋いでいる。

2022年やりたいこと

もっと自分のために生きたい。

おわりに

『劇場版 少女☆歌劇 レヴュースタァライト』の配信が始まっているのでみなさんみましょう。

Viewing all 97 articles
Browse latest View live