nashcft's blog

時々何か書く。

Android project で Gradle 8.0 に更新するために Java toolchain の設定をする

ある日 Android project の Gradle を 7.6 から 8.0 (この記事を書いている時点での最新は 8.0.1) に更新したところ以下のようなエラーと共にビルドに失敗した:

'compile{Variant}JavaWithJavac' task (current target is 1.8) and 'kaptGenerateStubs{Variant}Kotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.
Consider using JVM toolchain:https://kotl.in/gradle/jvm/toolchain

この時の target JVM まわりの設定は以下のようになっていた:

android {
  // ...
  compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = "1.8"
  }
}

つまり target JVM を 1.8 に設定しているのに KaptGenerateStubs task は 17 を target として実行されてしまっているという状況のようだ。このビルドを実行した時の JDK のバージョンは 17 だったので build.gradle の設定を無視して実行環境上の JVM のバージョンを参照しているように思われる。ついでに JVM toolchain なるものを使いなよという提案もされている。

これをどうにかしないことには Gradle を 8.0.x には上げられないので対処したのだが、その過程で JVM toolchain (Gradle の docs では "Java toolchain" と呼ばれていたので以後 Java toolchain とする) を設定することになったのでそのまわりについて調べたことをざっと記録していこうと思う。

kapt が jvmTarget を参照しなくなった

Gradle 8.0 の release note と Kotlin 1.8 の what's new を読んだ感じ以下のような状況らしい:

  • Gradle 8.0 から Kotlin DSLAPI level が 1.8 になった
  • Kotlin 1.8 から Java と Kotlin の compile task で target JVM が一致していない場合の反応をエラーとするようにした
  • kapt が jvmTarget の設定を見なくなったので、これと合わせるように設定していた compileJavaWithJavac task の target compatibility とずれてビルドエラーが発生した

docs.gradle.org

kotlinlang.org

kapt の挙動変更については Kotlin の what's new に記載がなかったのでこのエラーについて何か情報がないか検索したところ、 YouTrack にそれそのものな issue があった:

現時点のやりとりにおける JetBrains の中の人の見解は以下のような感じ:

  • Kotlin 1.8.0 から kapt が jvmTarget, というか kotlinOptionscompilerOptions を参照しなくなったのは意図的な変更で、バグという認識をしていない
  • jvmTarget による指定をやめて Java toolchain を使ってほしそう

報告者も言っているように意図的な、しかも後方互換性を破壊するような挙動の変更ならちゃんと what's new で言及してよと思うが、document を読んだ感じ Gradle としても target JVM まわりの設定は Java toolchains に寄せてほしそうだし、そういう気持ちなら Java toolchain を設定する方向でいいか、となった。

JavaVersion sourceCompatibility
Java version compatibility to use when compiling Java source. Default value: language version of the toolchain from this extension.
Note that using a toolchain is preferred to using a compatibility setting for most cases.

docs.gradle.org

ところで報告者は Android Studio に同梱された JDK を使ってビルドしたいが JVM target は固定したい (≒ ビルドに使う JDK のバージョンと異なる target を指定したい)と言っており、この場合 Java toolchains による暗黙的な設定に置き換えると指定した toolchain のバージョンに target JVM が揃えられてしまうので要求を満たせない。このような要求に対する現状の workaround は報告者のコメントなどを見るに KotlinCompile 系の task に対して configureEach で target JVM の指定を適用するという方法くらいのようだ。ただし報告者によるとパフォーマンスの問題があったり build script の構成によっては上手くいかないこともあるらしい (未確認):

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = "<指定したい jvm version>"
    }
}

KSP も同様

KSP だとどうだろうと思って、以下の repository がちょうどいい構成だったのでこれで Gradle を 8.0.1 にして確認したところ同様のエラーが発生した:

github.com

'compileDebugJavaWithJavac' task (current target is 1.8) and 'kspDebugKotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.
Consider using JVM toolchain: https://kotl.in/gradle/jvm/toolchain

ということで ksp しか使っていない project でも Java toolchain の設定をするか上記の workaround を入れるかして対処する必要がある。

Java toolchain の設定をする

Java toolchain とは

Java toolchain とは大雑把に言うと JVM 系の project のビルドや実行に使われるツール群 (javac とか java とか) のことで、 Gradle 6.7 から Gradle を実行しているものとは別の toolchain を project や task の単位で指定できるようになったとのこと。異なるマシン上でも実行環境を揃えたり、 project 内で異なる Java version での task 実行を行ったり、 Gradle がサポートしていないバージョンの Java で project / task の実行をしたりといったことを目的としているようだ。

Document はこれ:

docs.gradle.org

6.7 時点の話なので 8.0 での挙動等と異なる点はあるけどこの記事も読むといいかも:

progret.hatenadiary.com

設定の記述やルール

今回は基本的には project level での設定をすれば十分なのでその方法のみ紹介する。 Task level での切り替え設定をしたい場合は document を読んでほしい。

Java toolchain の設定は java extension block で以下のように記述する:

java {
  toolchain {
    languageVersion.set(JavaLanguageVersion.of(17))
    vendor.set(JvmVendorSpec.ADOPTIUM)
  }
}

設定できる項目は以下の3つ:

  • バージョン (languageVersion): toolchain に指定する Java のバージョン
  • 配布元 (vendor): 使用する JDK の配布元
  • VM の実装 (implementation): 主に OpenJ9 を使用したい時に設定する
    • Android project だとまあまあ関係ない話かも

これらの設定には有効性の概念があり、下記の2パターンの内のどちらかに該当するものが有効とされる。そうでない設定は無効と判定されビルドエラーとなる:

  1. 何も指定がない
  2. languageVersion の指定が含まれている

Android project では基本的に languageVersion の設定だけで十分だと思っていて、もし distribution にこだわりがある or そこまで実行環境を揃えておきたいという場合には vendor も書くという風になるだろう。

また、toolchain の設定は Kotlin Gradle plugin からも行うことができ、 distribution の指定が必要ない場合はより短い記述で設定可能になっている:

kotlinlang.org

kotlin {
  jvmToolchain {
    // この block の中の this は java.toolchain と同じ JavaToolchainSpec なので書くものは同じ
    languageVersion.set(JavaLanguageVersion.of(17))
    vendor.set(JvmVendorSpec.ADOPTIUM)
  }

  // バージョン指定だけで良い場合は以下のように書ける
  jvmToolchain(17)
}

このようにして toolchain (のバージョン) の指定を行うと、 Java における sourceCompatibilitytargetCompatibility, Kotlin における jvmTarget を設定しなかった場合に指定された toolchain のバージョンが target JVM として使用されるようになる。

Toolchain に使用する JDK/JRE の選択と自動ダウンロード

Java toolchain の指定がなされた project で build 等を実行すると、ローカルにインストールされている toolchain たちの中から指定に該当するものを自動で選択し、それを用いて各 task を実行する。ローカルにインストールされている toolchain の検知対象については document に以下のように書かれている:

Package manager でインストールしたものが使えるので、普段 JDK の管理でそれらを使用している人ならそのまま使いまわせて便利。

ところで、自動検知の結果ローカルに該当する toolchain が見つからなかった場合、これまで紹介した設定だけだと該当なしということでエラーになってしまう。実は元々は該当なしだった場合 AdoptOpenJDK (現: Adoptium) が自動でダウンロードされるようになっていたのだが、 8.0 でその機能が削除されてしまっている。

docs.gradle.org

ただし、「ローカルに指定に該当する toolchain が無かった場合にはダウンロードして取ってくる」という機能自体は 8.0 以降も存在しており、 download repository の設定を記述することで有効になる。 Download repository の設定は基本的には以下のような感じで settings.gradle に resolver について書くことになる。 Toolchain resolver は plugin として自分で作ることもできるが、既に Foojay Toolchains Plugin というものが公開されており、一般的な JDK distribution を使うならこの plugin を使用すれば事足りる。

github.com

pluginManagement {
  // ...
}

plugins {
  id("org.gradle.toolchains.foojay-resolver") version("0.4.0")
}

toolchainManagement {
  jvm {
    javaRepositories {
      repository("foojay") {
        resolverClass.set(org.gradle.toolchains.foojay.FoojayToolchainResolver::class.java)
      }
    }
  }
}

Download repository に foojay しか使わない場合は foojay-resolver-convention plugin を使うことで toolchainManagement block の記述を省略できる:

// foojay しか使わないならこれを追加するだけで OK
plugin {
  id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
}

自動ダウンロードでの toolchain の取得を当てにする場合、あらゆる種類の JDK を取得できるわけではないので注意。例えば foojay は JavaVendorSpec で定義されているすべての vendor をサポートしているわけではないし、それぞれの vendor でダウンロード可能な JDK のバージョンも違いがある。欲しい toolchain が取得可能かは使用する resolver plugin の対応状況や、 asdf など package manager がそれぞれの vendor からどのバージョンを取得可能かのリストなどを確認するとよい。とはいえ最新の Java に追従しているわけではない Android では基本的には LTS のバージョンを使うことになるだろうし、 LTS のものなら入手できないことはまずないので、気にするとしたら使いたい vendor で JDK 8 が配布されてるかどうかくらいか。

参考 (asdf-java が取得可能な JDK の一覧):

github.com

Android project 向けの workaround

Kotlin の document の Configure a Gradle project のページを見たらわかるが、 toolchains support の冒頭に Android project に関する注意が書かれていて、 Java toolchains のサポートは AGP 7.4.0 以上だということの他に toolchain を設定しても Java の compile task の target JVM に反映されないという現状の不具合について issuetracker へのリンクと対処法が示されている。

こういう状況なので現時点では toolchain の設定で指定した JVM のバージョンに対応する JavaVersion の値を compileOptions で指定する必要がある:

kotlin {
  // 例えば Java toolchain の設定が 11 なら
  jvmToolchain(11)
}

android {
  // ...

  compileOptions {
    // これらの compatibility options も 11 に合わせる
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
  }
}

Robolectric を使っている場合について

Robolectric は Android の target SDK 29 以上で実行する場合実行環境に Java 9 以上を要求する:

github.com

このため Java toolchain の設定に乗り換える際に toolchain のバージョンを単純に 8 に指定するだけだと Robolectric を使用したテストの実行でエラーになってしまう。この問題を対処するには

  1. toolchain のバージョン指定を 9 以上にする (= target JVM を 9 以上に上げる)
  2. robolectric.properties で実行時の target SDK を 28 以下にする

の2通りの方法が考えられる。

Target JVM のバージョンを上げる方法に関して、 application project の場合はあまり問題にならないと思うので軽率に上げてしまって良さそうだが、 library project の場合は利用者の target JVM より高くなると使えなくなってしまうので、移行期間を設けたり test の toolchain だけ 9 以上になるように設定したりするなど上手くやる必要がある。個人的には普通の Android アプリ開発で target JVM 1.8 にこだわる理由は特にないのではと思っているのでみんな軽率に 11 に上げてしまっていいんじゃないかと思っている。17 は R8 がまだ対応しきれていないっぽいのでまだいいかなという感触。

sdk property で対処する方法は簡単だし外への影響がないのだが、 minimum SDK が 29 以下の間しか使えないので一時凌ぎとして捉えた方が良い。

CI について

(2023-07-19 内容を変更、元の記述は間違った内容を含むので削除)

CI 環境のセットアップについては自動ダウンロードを有効にしている場合でもそうでない場合でもまず Gradle を動かすための JDK を task 実行前に用意しておく必要がある。自動ダウンロードを無効にしている場合はさらに project で指定した version の Java tool chain をダウンロードするステップが追加されるが、これに関しては CI が複数の JDK を配置させられるような機能を持っていれば*1それを使うなり package manager をインストールしてそれ経由でダウンロードするなりで実現できるだろう。 Gradle による Java toolchain の自動検知対象となる場所以外に配置しても、下記の property に配置場所の path を指す環境変数を指定して gradle.properties に追記することで Gradle に認識させることができる:

org.gradle.java.installations.fromEnv=<JDK の path を指す環境変数 (,区切り)>

できる限り手元の開発環境でも CI 環境でも使う toolchain を揃えたいという場合は自動ダウンロードに任せてしまった方が設定の手間もなくポータブルだが、そこまで厳密な運用を必要としないなら好みの範囲かと思う。

おわりに

kapt / ksp の挙動変更については疑問が残るが、 Java toolchain の設定自体は便利機能だし、当初思っていたより設定の手間がずっと少なかったので、ビルド環境に関する制約がなければ設定の移行をしてしまって良いと思う。今は Java toolchain の設定を導入したくないとしても、これまで通りの target JVM の指定方法を維持するための workaround も一応存在するので、 workaround を入れてとりあえず Gradle 8.0 に上げてから最終的にどうするか考えるという手段を取ることもできそう。なんにせよ Android Gradle Plugin 8.0 から要求される Gradle の最低バージョンが 8.0 になるので、今のうちから Gradle のバージョンを 8.0 以上に上げられるようにしておきたい。

developer.android.com

それで Java toolchain の設定をするのに結局どういう変更をしたらいいのかというのが散らかってしまっていると思うので、私が行った build script の変更の抜粋を以下に置いておく。今回対象となった project は target JVM を 11 にしても問題なかったのでそのように修正しているが、移行したいが少なくとも build の target JVM は 1.8 でないと問題がある + Robolectric を使っているという場合は task level での toolchain の設定で乗り切れるはずなので調べて試してみてほしい。

build.gradle

+ kotlin {
+   jvmToolchain(11)
+ }

  android {
    ...
    compileOptions {
-     sourceCompatibility = JavaVersion.VERSION_1_8
-     targetCompatibility = JavaVersion.VERSION_1_8
+     // https://issuetracker.google.com/issues/260059413 が修正されたら削除
+     sourceCompatibility = JavaVersion.VERSION_11
+     targetCompatibility = JavaVersion.VERSION_11
    }

-   kotlinOptions {
-     jvmTarget = "1.8"
-   }
    ...
  }

settings.gradle

  ...
+ plugin {
+   id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
+ }
  ...

調査メモ:

scrapbox.io

*1:例えば GitHub Actions の setup-java では複数の JDK のインストールを指定できる: https://github.com/actions/setup-java/blob/4fb397523b853fa75ed64fd1d10a967e1eb3148a/README.md#install-multiple-jdks

Android 13 から導入された per-app language preferences について

文脈

github.com

触ったのでついでに調べてまとめることにした。

Per-app language preferences って何

端末全体の言語設定とは別にアプリ毎の言語設定を system 側で管理してくれる機能。

developer.android.com

System settings から設定できるようにする

Android 13+ で有効化するためにやることは以下の2つ:

  • res/xml/locale_config.xml を作成する
  • AndroidManifest の application 要素に android:localeConfig 属性を追加し、作成した config xml を指定する

細かいことはドキュメントを読んでもらうとして、 locale_config.xml は以下のような内容のファイルになっている。

<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="en"/>
    <locale android:name="ja"/>
    <locale android:name="zh-CN"/>
    ...
</locale-config>

ドキュメントで言及のある build.gradleresConfigs は単に per-app preferences で指定したサポートする言語とアプリに含める言語 resource を一致させるための設定なので、書かなくても有効化される。

上記の変更によって per-app language preferences が有効化されると、以下の場所からアプリの使用言語を設定できるようになる:

  • Settings > Apps > 対象のアプリ > Language
  • Settings > System > Languages & input > App Languages > 対象のアプリ

アプリ独自の言語設定機能と連携させる

以前からアプリ内の言語設定を変更する機能を独自に実装していたアプリや、言語設定のためにいちいち Settings に移動させずに自アプリ内で操作を完結させたいという要求に対して、この per-app language preferences と連携させるための API が追加されている。

追加された API は framework の LocaleManager と AndroidX AppCompat library の AppCompatDelegate#getApplicationLocales/AppCompatDelegate#setApplicationLocales で、 LocaleManager は added in API level 33 なので当然公式の推奨は AppCompat の方になる。 AppCompat library の方は 1.6.0 で追加されている (2022-09-23 時点で RC01)。

することに特に難しいことはなく、現在設定されている locale を取得する時に getApplicationLocales を呼び、 locale を変更する時は設定したい言語を LocaleList / LocaleListCompat として setApplicationLocales に渡すだけで OK。ちなみに、言語設定が system default の時は空の locale list が取得でき、 system default に設定したい時は空の locale list をセットすることで実現できる。

// アプリの言語が「日本語」に設定されているとする

// 中身は [ja]
val currentLocales = AppCompatDelegate.getApplicationLocales()

// アプリの言語設定が「English」になる
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("en"))

// アプリの言語設定が「System Default」になる
// LocaleListCompat.forLanguageTags("") も getEmptyLocaleList() と同義になる
AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList())

// 中身は []
val localesChangedToDefault = AppCompatDelegate.getApplicationLocales()

以下は DroidKaigi2022 のアプリで実装したもののデモ動画。アプリ内で変更した言語設定が system settings の方の言語設定にも反映されていることがわかる。

AppCompat の API を使う際の注意点

追記 (2022-09-24): Android 12 以下での挙動の記述が誤っていたので修正

あまり無いケースだと思うが AppCompatDelegate の方を使う場合の注意点として、API reference にも記述があるように、getApplicationLocales/setApplicationLocalesActivity#onCreate 以降でのみ呼ぶようにすべきというものがある。これは Android 13+ 向けの実装の都合で、内部で LocaleManager を取得するのに必要な Context が手に入るようになるのが onCreate 以降*1となり、それまでは呼んでも実際の get/set が実行されないから。 Android 12 以下の場合は get/set で受け渡しする値は AppCompatDelegate 内部のメンバとして管理されているため、 onCreate 以前でも AppCompatDelegate との値の受け渡しは正常に行うことができる。この際、実際に framework 側に設定が同期されるのは onCreate 呼び出し時である。後述するがこの挙動を利用して Android 12 以下向けに後方互換対応をする場合がある。

また、これは地味かつ当然といえば当然な話だが、 AppCompat の API を使う時はそれが呼ばれる Activity が AppCompatActivity を継承したものである必要がある*2AppCompatActivity が用いられなかった場合の挙動は大体上記と同じだが、こちらに関しては Android 12 以下でも framework に言語設定が同期されなくなるので設定の変更がアプリに反映されない。

この辺の仕組みの詳細は長くなるし横道に逸れるので書かないが気になる人は AppCompatDelegatesAppContextsActivityDelegates に入る値はどこから来るかや framework との同期はどこで行われるかを追いかけてみるとわかると思う。

Android 12 以下でも使えるようにする

Android 12 以下には per-app language preferences の機能が存在しないので、「アプリ独自の言語設定機能と連携させる」で書いたように AppCompat library を使ってアプリ内に言語設定の機能を実装する必要があるが、これだけだとアプリのプロセスが終了する度に設定した言語の情報が揮発してしまうので、アプリ内で設定情報を保存しておく必要がある。これに関しては AppCompat library を使った自動保存を opt-in するか自前の保存機構を使って起動時に連携するかという二通りの対応方法がある。

言及箇所へのリンク:

自動保存の opt-in

Manifest に以下のように AppLocalesMetadataHolderService を登録するとライブラリの方で上手くやってくれる。ただしこの設定をすると blocking な disk read/write が発生するようで、 StrictMode で検知するようにしている場合は引っかかってしまうとのこと。 DroidKaigi2022 アプリは自前の言語設定機能をまだ持っていなかったし、新規にローンチされるものでデータの引き継ぎを考慮する必要もなかったのでこちらを採用した。

<application
  ...
  <service
    android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
    android:enabled="false"
    android:exported="false">
    <meta-data
      android:name="autoStoreLocales"
      android:value="true" />
  </service>
  ...
</application>

自前の保存機構と連携

既に自前の言語設定機能を持っていたり、 blocking な disk read/write が許容できないなどの理由で AppLocalesMetadataHolderService を manifest に登録しないか、した上で autoStoreLocalesfalse した場合はこちらの選択肢となる。やることは以下の通り:

  1. Activity の Activity#onCreate が呼ばれる前*3に保存されている言語設定を読み出して AppCompatDelegate#setApplicationLocales に与える
  2. 上記の操作を Android 13+ で実行されないように制御する
  3. アプリ操作での setApplicationLocales 実行時に自前の storage にも設定を保存する

Android 13+ では同期処理を実行させないようにするのは、 per-app language preferences の方で言語設定を保存しているので初期化する必要がないのと、1. の時点で呼んでも機能しないから。

バイスの OS version 更新時の設定引き継ぎ

アプリ利用中にデバイスの OS が 12 から 13 に更新された際にそれまでアプリ内で管理していた言語設定を per-app language preferences に引き継ぐ方法について:

  • autoStoreLocales を有効にする対応をした場合は AppCompat library が自動的に同期してくれる
  • 自前の storage で管理していた場合は適当なタイミングで設定を取り出して AppCompatDelegate#setApplicationLocales に与える one-time の処理を仕込んでおく

まとめ

  • Android 13 以降でだけ動けばいいなら xml 作って manifest に設定すれば OK
  • 12 以下でも同じような体験をさせたい場合はアプリに言語設定機能を作って AppCompat library を使う、12 以下向けの設定保存をどうするか考える
    • AppCompat library に設定保存まで任せると実装や OS version up 時の引き継ぎが楽、ただし blocking disk R/W を許容する必要がある
    • 設定保存をアプリで実装して設定の取得/変更をする時だけ AppCompat の API を呼ぶ方法もある
  • Android 12 以下から 13 以降への更新時に言語設定を per-app language preferences に引き継ぐことを考慮しよう

*1:より正確には androidx.activity.ComponentActivity#onCreate 以降

*2:実際には AppCompat の API が呼ばれるまでの経路中で AppCompatActivity が起動されていれば呼び出し元が乗ってる Activity 自体は AppCompatActivity でなくてもよさそう感がある

*3:Application#onCreate とか Activity#attachBaseContext とか

Android/Kotlin で GraphQL バックエンドと会話する時に嬉しくないと思ってることメモ

ダラーっと tweet してたもののまとめ
Android で native = Apollo Kotlin を client library として使う想定で書いている。他の cross-platform な開発環境 (React Native とか Flutter) だと事情は異なる、はず (経験ないので)。

Apollo Kotlin の Error まわりの実装:

GraphQL の error の format に関する仕様

最近触った Gradle 関連の機能

備忘録のようなもの。

Gradle

Version catalogs

docs.gradle.org

Project root の build.gradleext に生やしたり buildSrc や composite build 用 module 内に object declaration で定義されたりするような慣習のあった依存ライブラリの定義用の機能。 7.0 で experimental feature として導入され、7.4 で stable になった。

定義の方は settings.gradle(.kts) に書く方法と .toml ファイルに書く方法、複数の catalog を作ったり toml の方は読み込むファイルの指定をしたり定義した library 依存をまとめて指定できる bundle という設定があったりと色々あるが、全部 docs の内容の写しになってしまうし、それをつらつらと書くとしても量が多くて面倒なので docs を読んでほしい。自分の躓き体験としては toml ファイルで定義するときはファイルの置き場がデフォルトでは gradle/ なのだが、それに気づかず project root に置いてて生成されなくて悩んでたというのがあった。

これを設定すると ProjectgetExtensions() (もしくは extensions) で取得できる ExtensionContainer に定義した名前で LibrariesFor<定義した名前> が登録されて、そこから catalogs に登録した情報にアクセスできるようになる。それぞれの値に対する alias name には -, _, あと . を使うことができるが、これらは全て catalog の階層の区切りとして扱われる。

例えば以下のように定義した場合:

[libraries]
kotlin-stdlib = { ... }
kotlin-stdlib-jdk8 { ... }

kotlinx-coroutines-core = { ... }
kotlinx-coroutines-android = { ... }
kotlinx-serialization-json = { ... }
kotlinx-serialization-protobuf = { ... }

呼び出し側は以下のようになる:

dependencies {
  implementation(libs.kotlin.stdlib)
  implementation(libs.kotlin.stdlib.jdk8)

  implementation(libs.kotlinx.coroutines.core)
  implementation(libs.kotlinx.coroutines.android)
  implementation(libs.kotlinx.serialization.json)
  implementation(libs.kotlinx.serialization.protobuf)
}

ところで呼び出し側は補完が効くようになるので便利だが、呼び出し箇所から定義にジャンプできない (生成された LibrariesFor<定義した名前> 内の get method に飛んでしまう) ので、 object declaration で定義してた状態から移行するとその点に関しては不便を感じる。

ある程度決まった文法をしているけど、要は適当な String value を名前をつけて登録できる機能みたいな見方ができるので使用には多少の自制心を求められそうな気もする (version はその気配を特に感じる)。

Type-safe project accessors

docs.gradle.org

Project 内の module への依存 (こういうのを "project dependency" と言うそうだ) を記述するときは project(":module:path") みたいな書き方をしていたが、 path が String のリテラルってまあ面倒だよねということで 7.0 から experimental feature として導入された。

Version catalogs と違ってこっちは 7.4 の時点でまだ experimental から抜けてないので使えるようにするには settings.gradle(.kts) に以下の記述を追加する必要がある:

enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")

これが有効になると、 version catalogs と同じような感じで extensions から "projects"RootProjectAccessor が取れるようになり、依存の記述を以下のようにすることができる:

dependencies {
  // これが
  implementation(project(":module:path"))
  // こう
  implementation(projects.module.path)
}

区切りの法則が version catalogs と異なり、 path の : 毎に . 区切りとなって、 名前の -_ は camelCase に変換される:

// これが
project(":path:to:awesome-module")
// こんな感じ
projects.path.to.awesomeModule

仕組みは概ね version catalogs と同様な感じ。なので jump しても生成された class の method に飛ばされるだけだが、こちらは特に飛ばされてほしい先が無いので version catalogs ほど不便に感じないかも知れない。

これと version catalogs 関連のコードを読みたかったら org.gradle.api.internal.catalog package まわりから始めることになりそう。

Precompiled script plugins

docs.gradle.org

buildSrc の src 内に置いた .gradle(.kts) の gradle script が各 module の build.gradle で plugin として参照できるよって話。上記の docs と以下の記事を読めば何となく使えるようになる。

mike-neck.hatenadiary.com

AGP

Publishing DSL

developer.android.com

androidstudio.googleblog.com

AGP 7.1.0 から導入された。 Maven publishing のために生成するsoftware component を自動生成ではなく自分で指定する方式に変えるようで、そのための DSL だそうだ。 AGP 7.1.0 の時点でこの設定を行わずに maven publishing plugin を使用している場合以下の warning が表示される:

WARNING:Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8.0. To opt-in to the future behavior, set the Gradle property android.disableAutomaticComponentCreation=true in the gradle.properties file or use the new publishing DSL.

使い方は簡単で、 android {} の中で publishing {} を呼び出して、その中で publish したい build variant を指定すればOK。

android {
  // ...
  publishing {
    singleVariant("release") {
      // Sources jar を生成させるための option
      withSourcesJar()
      // JavaDoc jar を生成させるための option
      withJavadocJar()
    }
  }
}

詳しいことは JavaDoc 読むのが良い。

JavaDoc がやけに充実しているのは Android Developers の maven publishing plugin のページの内容が更新されるまでの繋ぎとしてここに書いているからとのこと。

https://cs.android.com/android-studio/platform/tools/base/+/2e1ff8e3b05a4fcaad12066c6f38633b6875a2ae

As we have to wait the feature goes into beta to update the DAC page https://developer.android.com/studio/build/maven-publish-plugin, it would be nice to have more java doc including some code samples to introduce this new feature to users. And we could have a brief note in our release notes and point users to the java doc. Once we are able to update the DAC page, we could simplify the java doc.

developer.android.com

2021年振り返り

過去の

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

Read more

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

起こってること

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:理由は調べてない

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

さっき投稿した記事でエラーの原因を調査していたときに 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 most Android 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 使えない雰囲気があって謎。