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

最近触った Gradle 関連の機能

$
0
0

備忘録のようなもの。

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


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

$
0
0

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

Apollo Kotlin の Errorまわりの実装:

GraphQL の error の format に関する仕様

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

$
0
0

文脈

github.com

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

Per-app language preferences って何

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

developer.android.com

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

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

  • res/xml/locale_config.xmlを作成する
  • AndroidManifest の application要素に android:languageConfig属性を追加し、作成した 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  ...<serviceandroid:name="androidx.appcompat.app.AppLocalesMetadataHolderService"android:enabled="false"android:exported="false"><meta-dataandroid: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 project で Gradle 8.0 に更新するために Java toolchain の設定をする

$
0
0

ある日 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 について

諸々の事情で CI 環境上で自動ダウンロードを試していないのだが、ローカルで動かしてみた感触だとダウンロード速度が割と遅いように思うので、 CI で動かすときはそちらで提供されている機能を使って予め使用する toolchain に該当する JDKをインストールしておいた方がパフォーマンスが良いかもしれない (たとえば GitHub Actions なら setup-javaを使うとか)。実際はパフォーマンス差が無かったよというような場合は、環境のセットアップの手間が減るので 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

Jetpack DataStore で保存する値はキャッシュされている

$
0
0

タイトルの通りで、実用性を考えたらそれはそうでしょというものなんだけど、確認のために実装を読んだのでそのメモ。というのをしばらく放置していたら実装がガッと書き換えられてしまったので書き換え以前 (= 1.1.0-alpha01時点) と書き換え後両方の実装を見ていくことにする。

1.1.0-alpha01 時点の実装

1.1.0-alpha01時点では DataStoreの実装 class は single-process 向けと multi-process 向けの2種類ある:

このそれぞれが downstreamFlowという StateFlow を持っていて、ここに保存しているデータを保持している。

@Suppress("UNCHECKED_CAST")
privateval downstreamFlow = MutableStateFlow(UnInitialized as State<T>)

Read 側に当たるDataStore.dataはこの downstreamFlowの状態がデータ保持済みでなかったり整合性が取れていなかったりする場合にストレージアクセスの依頼を行い、あとは downstreamFlowの中身を見続けるというようになっている。

e.g.) SingleProcessDataStore:

overrideval data: Flow<T> = flow {
    /**     ...     */val currentDownStreamFlowState = downstreamFlow.value

    if (currentDownStreamFlowState !is Data) {
        // We need to send a read request because we don't have data yet.
        actor.offer(Message.Read(currentDownStreamFlowState))
    }

    emitAll(
        downstreamFlow.dropWhile {
            // ...
        }.map {
            when (it) {
                is ReadException<T> ->throw it.readException
                is Final<T> ->throw it.finalException
                is Data<T> -> it.value
                is UnInitialized -> error(
                    "..."
                )
            }
        }
    )
}

Write 側の DataStore#updateDataを追っていくと最終的にはストレージに新しい値を書き込んだ後にその値で downstreamFlowを更新している。 ストレージアクセスは actor を介して行われるので追うのが少し手間だが、 SingleProcessDataStoreでは transformAndWrite, MultiProcessDataStoreでは writeDataが実際に downstreamFlowを更新している関数となる。

SingleProcessDataStore:

// downstreamFlow.value must be successfully set to data before calling thisprivatesuspendfun transformAndWrite(
    transform: suspend (t: T) -> T,
    callerContext: CoroutineContext
): T {
    // ...val curDataAndHash = downstreamFlow.value as Data<T>
    curDataAndHash.checkHashCode()

    val curData = curDataAndHash.value
    val newData = withContext(callerContext) { transform(curData) }

    // Check that curData has not changed...
    curDataAndHash.checkHashCode()

    returnif (curData == newData) {
        curData
    } else {
        connection.writeData(newData)
        downstreamFlow.value = Data(newData, newData.hashCode())
        newData
    }
}

MultiProcessDataStore:

// Write data to disk and return the corresponding version if succeed.internalsuspendfun writeData(newData: T, updateCache: Boolean = true): Int {
    var newVersion: Int = 0// ...
    storageConnection.writeScope {
        // ...
        newVersion = sharedCounter.incrementAndGetValue()
        writeData(newData)
        if (updateCache) {
            downstreamFlow.value = Data(newData, newData.hashCode(), newVersion)
        }
    }

    return newVersion
}

書き換え以降の実装

注意: 本節の内容はリリースされていない開発中のコードに関するものなので、今後の変更によってなかったことになる可能性がある

2月の下旬ごろに DataStoreの内部実装の大きな変更が行われた。それによって DataStoreの実装 class は1つ (DataStoreImpl)に統一され、 single-process / multi-process の切り替えは新しく作られた interface InterProcessCoordinatorの実装で行われるようになった。また、キャッシュ部分は DataStoreの実装 class が持つ StateFlow だったのが DataStoreInMemoryCacheとして分離された。

変更に関する主な CL:

ということで、この変更後の状態でキャッシュにの仕組みについて追いかける時に見るのは以下の class になる:

DataStoreImplMultiProcessDataStoreの実装をベースに細かい変更が加えられたものというのが現在の状態で、キャッシュ関連は保持している値の取得や更新が downstreamFlowから inMemoryCacheに変わった他は流れを追う分には変わりなく読めると思う。

DataStoreInMemoryCacheは、以前の downstreamFlowに相当する StateFlow の保持と値の更新に関するちょっとした処理を持った helper class で、 multi-process のためのケアを追加することでコードが大きくなったので切り出したみたいな雰囲気。

おわりに

というわけで DataStore の内部実装で値のキャッシュを持っており DataStore.dataを 動かす度にストレージまで読みに行くわけではないので、アプリ側でキャッシュする仕組みを持つ必要はないことがわかった。冒頭にも書いたけどそれはそうという感想しか出てこなくて締まらないので関係ない話をすると DataStore のリリースは随分されていないのでそろそろ何か出してほしいですねという気持ちがある。あと保存データを暗号化する機能を組み込んでほしい。

Apollo Kotlin: エラーハンドリングのプラクティスと union type とコード生成方式

$
0
0

以前 Androidで GraphQL あまり嬉しくないのでは的な tweetをいくつかしていて、その中でエラーの扱いに困るなというものがあった。

https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format

大雑把にいうと top level の errors field は schema に載らない (= コード生成の対象にならない) ため結局エラー用の API document みたいなものを用意する必要が出てくる上に、アプリケーション由来の情報を入れるとなると errors.extensions field を使うことになるが、これは実質任意の構造の object なので Kotlin 上では Map<String, Any?>?の中を探るみたいな扱いになり面倒というものだった。

アプリケーション由来のエラーは schema で表現する

その後チームでエラーハンドリングどうしましょうを考える機会があって調べたり教えてもらったりで以下の記事を読んだ。

productionreadygraphql.com

sachee.medium.com

techblog.gaudiy.com

これらの記事を要約すると top level の errors field はシステム由来のエラーのみを扱うようにし、アプリケーション由来のエラー*1は schema 上で型として表現すべし、一箇所で複数種類のエラー状態の発生が起こりうる場合は union typeを使おう、みたいなことが書いてある。この考え方ならそれぞれの operation に対してどのようなエラーが起こりうるのかやそれをクライアントでどのように扱うかも schema で管理共有できるようになるしクライアントサイドでもビジネスロジックに関わるエラーを安全に扱えるようになるので便利、じゃあこれでいきましょうとなり、この方針に沿って schema の構築が進められることになった。

Apollo Kotlin で union type を union type らしく使うにはコード生成方式を変える

デフォルトの生成モデルの問題点

ところで Apollo Kotlin で何も考えずに union type の値を取得しようとするとあまり嬉しくなくて、これがどういうことかはコードを見た方がわかりやすいと思うので以下で示していく。コード例としては上でリンクした GraphQL の union type のページに例として載っている query を使う (若干変更を加えている):

# schematypeQuery{search(text: String!): [SearchResult!]!}unionSearchResult=Human|Droid|Starship# それぞれの具体型の定義は省略# queryquerySearch($text: String!){search(text: $text) {...onHuman{nameheight}...onDroid{nameprimaryFunction}...onStarship{namelength}}}

この query に対して生成されるコードは、モデル部分だけ取り出すと以下のような構造になる:

dataclass Data(
  val search: List<Search>,
) : Query.Data

dataclass Search(
  val __typename: String,
  val onHuman: OnHuman?,
  val onDroid: OnDroid?,
  val onStarship: OnStarship?
)

dataclass OnHuman(
  // ...
)

dataclass OnDroid(
  // ...
)

dataclass OnStarship(
  // ...
)

見てわかるように、 response として与えられるリストの要素を表すモデル Searchは inline fragment で指定したそれぞれの型が個別の nullable な property として定義され、また型同士も独立した関係となっている。 Schema の定義上 SearchResultは non-null なので Human, Droid, Starship (Kotlin 上では OnHuman, OnDroid, OnStarship) のいずれかは存在するのだが、モデルとしては実際にインスタンスが作られるまでどれが入ってくるかわからないのでそれぞれの property は nullable で表さざるを得ないという感じ。これを扱うとなるとそれぞれの property の null check を行って non-null だったら取り出して云々、みたいになるが、 GraphQL の schema 上は大丈夫と言っても Kotlin 上ではそうとはわからないコードになるので見た目上安心度が低くなんか泥臭い雰囲気のものになってしまう。

val search = // query 実行して response.data.search 取ってくるreturn search.map {
  when {
    it.onHuman !=null->// ...
    it.onDroid !=null->// ...
    it.onStarship !=null->// ...
  }
}

Response-based なコード生成方式

こういうのは Kotlin なら sealed class/interface として扱いたくなるものだし何かやり方あるでしょと調べてみるとあった。 Apollo Kotlin v3 から追加された機能で、コード生成方式を responseBasedに指定することで上記のような inline fragment による field 指定を一つの property にまとめる形でモデル生成をしてくれるようになるというものだ。

www.apollographql.com

コード生成方式の指定は build.gradleapollo block 内で codegenModelsに値をセットすることでできる。デフォルトの生成方式にも operationBasedという名前がついていて明示的に指定することもできるが、実際には responseBasedを指定する時しか codegenModelsは使わないだろう。

apollo {
  service("service") {
    // ...
    codegenModels.set("responseBased")
  }
}

そして responseBasedで生成した query のモデル部分は以下のような構造になる:

dataclass Data(
  val search: List<Search>,
) : Query.Data {
  sealedinterface Search {
    val __typename: Stringcompanionobject {
      fun Search.asHuman() = thisas? HumanSearch
      fun Search.asDroid() = thisas? DroidSearch
      fun Search.asStarship() = thisas? StarshipSearch
    }
  }

  dataclass HumanSearch(
    // ...
  ) : Search

  dataclass DroidSearch(
    // ...
  ) : Search

  dataclass StarshipSearch(
    // ...
  ) : Search

  dataclass OtherSearch(
    // ...
  ) : Search
}

リストの要素 Searchが data class から sealed interface に変わり、 inline fragment で指定した型はその具体型として表現されている。これで無駄な nullable に煩わされることなく whenを使って具体型を特定して分岐させるという書き方ができるようになった。やったね。ところで OtherSearchという型が生成されているが、これは例えば interface で返す field になっていて、 interface で定義されてる field を取りつつ特定の具体型だったらこの field も、みたいな書き方をしている時に inline fragment などで指定した型にマッチしないものが取得された場合の受け皿として用意されているものと思われる。

responseBasedなコード生成に関しては inline fragment だけでなく自分で定義した fragment に対しても同じように機能したり field を merge してくれたりと他にも色々トピックはあるが、詳しくは上で紹介した公式ドキュメントを読んでほしい。 Apollo Kotlin のコード生成まわりは公式ドキュメント以外にも以下の記事や design document を読むと理解が深まるかもしれない。

www.apollographql.com

github.com

おわりに

GraphQL とやりとりする際のエラーハンドリングに関するアプリケーション由来のエラーを schema で表現するプラクティスと、それで union type を使う際に Apollo Kotlin ではコード生成方式を responseBasedを指定すると Kotlin 側でも sealed interface として扱えるようになるので便利という話をした。ハンドリングが必要なエラー状態を schema で表現するという schema 設計方針を採用できるなら GraphQL を Android/Kotlin で扱う上での嬉しくないポイントが一つ解消されるなあという所感なのだが、実際のところどのくらい普及しているプラクティスなのだろうか。

*1:エラーというか失敗シナリオと呼ぶべきか

Kotlin: runCatching と coroutine

$
0
0

内容的には以下の issue で議論されていることの抜粋のようなものだが、つまるところ現状 Kotlin Coroutines と runCatching (より詳細には runCatchingの block 内で suspend function を呼んだ場合) の食い合わせが悪い問題に対してどういう対処ができるのかについて、備忘としてまとめておく。

github.com

runCatchingは全ての例外を catch する

現時点のコード:

github.com

publicinlinefun<R> runCatching(block: () -> R): Result<R> {
    returntry {
        Result.success(block())
    } catch (e: Throwable) {
        Result.failure(e)
    }
}

runCatchingblockの実行中に発生した例外を全て捕捉し、 Resultとして返すという振る舞いになっている。 Result.failure(e)は catch した ThrowableResult.Failureで wrap して Resultに詰めて返すというもの。

Kotlin Coroutines の cancellation 復習

Kotlin Coroutines では coroutine が cancel された際にシグナルとして現在中断されている箇所で CancellationExceptionを throw し、これが起動中の coroutine 内を通り抜けていく。 runCatchingはこの CancellationExceptionも捕捉してしまうので、都度自分で rethrow 処理を実装しないと cancel シグナルの伝搬がそこで止まってしまって上層に cancel されたことが伝わらなくなったり、 cancel 時の振舞いが意図しないものになる可能性があったりといった不都合が出る。

CancellationExceptionの伝搬が上手くいかないとどうなるかについてはそこまで真面目に探していないこともあって今のところ良い感じにまとめられた内容の資料を見つけられていないが、冒頭の issue のコメントや以下のページから雰囲気は掴めると思う。

kotlinlang.org

medium.com

現時点の workaround

じゃあ毎回 CancellationExceptionだけ rethrow するのを書くかというとダルく、特別 cancel された時に実行したい処理を書くなんてことがある場合以外は CancellationExceptionの存在をロジック中で意識させたくないというのが正直なところだと思う。なので現時点では冒頭の issue で提案されているような runCatchingCancellationExceptionの catch/rethrow を追加した関数を定義して使うのが落とし所としては無難だろう:

suspendfun<R> suspendRunCatching(block: suspend () -> R): Result<R> {
  returntry {
    Result.success(block())
  } catch (ce: CancellationException) {
    throw ce
  } catch (e: Throwable) {
    Result.failure(e)
  }
}

このアプローチは実際に android/nowinandroidで採用されている。

github.com

ただ冒頭の issue に CancellationExceptionの中でも timeout 由来のもの (TimeoutCancellationException) は別で考慮した方がいいんじゃないか? という意見があり、何をどう catch するかは検討の余地がありそう。

また runCatchingThrowableを catch した所で ensureActive()を呼ぶのはどうかという案も出されているが、 ensureActive()を呼ぶには CoroutineScope, CoroutineContext, または Jobのいずれかが見えている必要があり使用に制限が出てくるので、個人的にはあまり筋が良いとは感じない。

メモ

Kover 触ったメモ

$
0
0

DroidKaigi 2023 公式アプリなどで触ったので所感などを書き残しておく。この記事の内容は Kover 0.7.3 時点の機能に基づいている。

書いてたら思ったより長くなったので先に結論を書いておくと「簡単なので使う場合は各自でいい感じにやっといてください」になる。

Kover とは

JetBrains が開発している code coverage の toolset。要するに JaCoCo みたいなやつ。今回は Gradle plugin について扱うが他に CLIも存在する。

使用メモ

使い方に関しては解説や要約が必要なほど多機能なわけではないので、何ができるかとかのちゃんとした説明は docs を読んでほしい。

Tasks

  • koverHtmlReport: 計測結果を html で出力
    • 手元で見る時はこれ
  • koverXmlReport計測結果を xmlで出力
    • codecov とかに食わせる時はこれ。
  • koverBinaryReport: 計測結果を binary file で出力
    • そういう形式を要求する何かに食わせる時用 (使ったことあるので具体例を知らない)
  • koverLog: 計測結果を task のログとして出力
  • koverVerify: 設定した rule を満たすかの検査
    • 設定については後述
    • rule を満たさない場合は task が失敗する

Android module に対しては上記の task 名の末尾に build variant が付く。

設定まわり

koverReportの中で記述する。 以下は大雑把な設定項目の説明:

  • filter: 計測対象の追加・除外の設定
    • class の指定に関しては fully qualified な class 名が求められるので package 部分から記述する必要がある
    • wildcard の記述で ***が使えるがこれらの違いは無いとのこと (?)
  • verify: verification task に関する設定
    • 評価する単位 ->全体、 class 毎、 package 毎
    • OK とする coverage の下限・上限
    • 計測値の基準 (lines / bytecode instructions / branches) と単位 (covered or missed に対する count / percentage)
  • defaults: default task に対する設定
    • filterverifyはこの中でも設定可能
    • 他に出力形式毎の諸々の設定をすることが可能
  • androidReports
    • Android module のそれぞれの build variant に対する設定
    • 記述できる設定については defaultsと同じ

Coverage report の集約

:foo:barがあったとして、 :fooの方の build.gradleで以下のように :barへの依存を追加すると、 :fooの方で kover task を実行した時に :barのものとまとめられた coverage report が作成される:

// foo/build.gradle
dependencies {
  kover(projects.bar)
}

kover()で依存に追加される module は 同様に kover gradle plugin を適用する必要がある。また、 report の集約を行う際、 koverReportによる設定は各 module のものではなく集約を行う (i.e. kover()による依存の設定を記述した) module のもののみが適用される。

所感

どこで report を集約するか

Kotlin/JVMや KMP で Androidを考慮しない module のみから構成される project であれば root project で集約して良いと思う。 Androidの考慮が入ると割と悩ましい。というのも root project は build variant を持っていないのでここで default report に build variant を merge させるということができないからである。別の場所で集約するとなると、 application project の場合は app module のような代表的な module が候補として検討できるが、 library project の場合は全てを集約した all-in-one module でも存在しない限りそういう候補は project 内で挙げにくく、 report 集約のためにわざわざ専用の module を作るかと言うとそれも微妙では? と感じる。また application project でも複数の app module を持っている、 product flavor の構成によって一つの build variant で全ての実装に対する coverage report を取得することができないといった場合には同様の課題が発生すると思われる。

現時点 (0.7.3) では複数の build variant に対する report を1つに集約する設定方法みたいなものは無さそうだが、複数の build variant をまとめた custom report variant を作成するという機能が以下の issue で検討されているので、将来的にはなんとかなるかもしれない。でも自分の持っていない build variant を解決できるものなのだろうか...?

github.com

設定の記述で楽をしたいが...

触ってみた感じの印象として今のところ Kover の DSLは単純で習得しやすい一方で素朴な書き方しかできず、素直に書くと楽したくなるところで面倒が発生するなあというものがある。具体的には report の集約を行っている場合、新しい module を追加する度に追加された module に kover gradle plugin を適用するのと集約 module の dependencies に kover(projects.newModule)を追加するのを忘れずに行う必要があるという点。これは地味にダルいし人間のやることなので忘れてしまうこともあるだろうしでどうにかしたいが、現時点では Kover DSL自体はこれを解決する機能を提供していない。

今年の DroidKaigi 公式アプリでは Kover の設定に関する convention plugin が実装されていたので、そこに上記の課題を何とかするための実装を追加して、 report を集約する module (i.e.:app-android module) 以外では Kover に関して気にしなくて良くなるようにした。

github.com

この追加実装では以下の3つの操作をしている*1:

  1. この convention plugin を適用した module に kover gradle plugin を適用する
  2. この convention plugin を適用した module 以外の module に kover gradle plugin を適用する
  3. この convention plugin を適用した module に対して、 2. の操作を行った module を report の集約対象として依存に追加する

ただし、レビューコメントでも指摘されているようにこのアプローチは Gradle の configuration cache 関連で将来導入されるであろう project isolation と思い切り衝突することをしている。現状これでも configuration cache を効かせられるし、今後このようなユースケースに対して project isolation を守りつつ同様の設定ができるようにになる APIが追加される可能性もあるが、あくまで今の内だけの手段として捉えておいた方が良いのではないかと思う。

Configuration cache と project isolation について参考:

ところで multi-module project での設定を簡単にするための機能を追加しようという issue が作成されているので、この設定記述の問題も Kover 側で何とかしてくれそうな雰囲気がある。 Custom report variant で root project から各 subproject の build variant をうまく扱えるようになったら Android project でも root project に設定を全部書いて済ませられるようになるのではないかと期待している。

github.com

とは言え今の時点ではどうすりゃいいのさとなるが、個人的には多少の手間を飲み込んで report 集約の依存の記述は集約場所の build.gradleに直接記述し、 plugin の適用と共通で使える koverReportの設定を convention plugin 化して、 DroidKaigi 2023 公式アプリの AndroidFeaturePlugin.ktみたいな各 module 共通で適用する plugin をまとめて適用する convention plugin に追加して使うくらいがいいんじゃないかなと思う。こうするとおまけとして kover gradle plugin を適用した module 全てに koverReportの設定が入るので module 単位で kover reoprt task を実行した時も集約した report を作る時と同じ設定で動くようになる。 Report ファイルの生成は module 個別に行うことはまずないだろうが、 verification は DSLに module 単位での評価という設定は無いので、 module 単位で基準を満たす or not を見られるようになるのは便利かもしれない... のだが、実は 0.7.3 の時点では全ての report variant 向けとなるはずの koverReport直下の verifyによる verification rule の設定が Android build variant に対応する report task には適用されないというバグがあり、 Android project ではこのように設定を共通化しても verification rule が共有できない場合がある*2:

github.com

おわりに

だらだらと Kover を触った所感を書いた。 APIがわかりやすく簡単に設定を書けるので、 JaCoCo で自作 merge task を作って頑張ってるようなところではこちらに移行した方が幸せになれるんじゃないかと思う。ただし Android project で flavor まわりの構成が複雑だと現時点ではうまくやるにはまだ機能不足に感じるので、そういう場合は頑張って gradle script を書いてどうにかなることを祈るか、もう暫く様子を見るのがいいかもしれない。

*1:2. と 3. に関して、実際は今回行った実装では考慮不足で module 以外の階層部分 (:core とか) にも操作が適用されてしまい色々無駄が発生しているのだが、結果に影響は出ないからいいやということでサボっている

*2:Default report task には問題の verification rule が適用されるので、 kover gradle plugin を適用する全ての module が同一の build variant を持っているならば、それを default task に merge した上で default report task を使用することで問題を回避し verification rule を共有できる


READ_MEDIA_IMAGES と READ_MEDIA_VIDEO の使用に制限がつくらしい

$
0
0

API level 33 からアプリ外のメディアファイルへのアクセスに必要な permission として READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIOが追加された*1が、これらの内 READ_MEDIA_IMAGESREAD_MEDIA_VIDEOの使用に関するポリシーの追加が 2023-10-25 付で発表されていた。

support.google.com

更新されたポリシーの previewは以下のページから確認できる。

support.google.com

これらのページによると、画像や動画のデータは "personal and sensitive user data subject to Google Play's User Data policy"であるためあまり無闇に対象となる permission を使わせないようにしたいので、デバイス上の画像や動画への広範なアクセスがアプリの主たる機能に直接関係するような場合だけこれらの permission を要求していいことにするよ、そのために READ_MEDIA_IMAGESREAD_MEDIA_VIDEOを要求するアプリに関しては使用が妥当なものかを審査するよ、とのこと。また、ファイル選んでアップロードするみたいなユースケースは上記の審査には通らないので system で提供している photo picker を使って permission 無しでのアクセスをするようにしてほしいということだそう。細かい話は以下のヘルプページの FAQ で色々書かれているのでそちらを確認してほしい。

support.google.com

スケジュール的にはだいたい来年中に対応を済ませましょうという感じ。

メジャーな対応ケースとしては、アプリに画像や動画の投稿・アップロード機能があって、そのために自作の picker を使っているとかで API level 33 以上で当該 permission を要求している場合に、 picker を Androidが提供している photo picker に置き換えて permission 要求の記述を削除するというものになるだろう。 Photo picker の使い方は activity result contract を作って投げるだけで簡単なので、公式ドキュメントに目を通せばその他の事項含めて割とすぐに置き換えることはできると思う。

developer.android.com

余談だが複数選択をした際に photo picker から返ってくるファイルの順番が選択順ではないので、それだと困るという人は以下の issue に vote しておくと早く対応してくれるかもしれない。

Dependabot: 設定の備忘録

$
0
0

最近 dependabot でライブラリ更新を自動化している repository の dependabot.ymlを手入れしていたのでそのメモ。

Gradle: settings.gradleで依存取得先を設定している場合に更新検知できないライブラリがある

Project の依存解決の設定を settings.gradledependencyResolutionManagementpluginManagementで記述している場合、一部のライブラリの更新検知ができないということが起こる。 Log を見てみると maven-metadata.xmlの取得先として Maven Central (plugin の場合はこれに加えて Gradle plugin portal) しか参照しておらず、たとえば AndroidJetpack library など GoogleMaven Repository に配置されているライブラリはバージョン情報の取得に失敗していることがわかる。

# e.g. AndroidX Core
updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> Checking if androidx.core:core-ktx 1.12.0 needs updating
  proxy | YYYY/MM/DD hh:mm:ss [xxxx] GET https://repo.maven.apache.org:443/maven2/androidx/core/core-ktx/maven-metadata.xml
  proxy | YYYY/MM/DD hh:mm:ss [xxxx] 404 https://repo.maven.apache.org:443/maven2/androidx/core/core-ktx/maven-metadata.xml
updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> Latest version is 
updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> Requirements to unlock update_not_possible
updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> Requirements update strategy 
updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> No update possible for androidx.core:core-ktx 1.12.0

build.gradleで依存解決の設定を記述している場合は設定した repository が maven-metadata.xmlの取得先に追加されていて期待通りにバージョン情報を取得できているので参照先の取得のために settings.gradleを読んでいないのだと思われる。 Dependabot のコードを何となく眺めた感じでもそうっぽい気がする。根拠にしてるのは以下のファイル:

github.com

これに関する issue はすでに報告されている:

github.com

以下の issue のコメントに workaround が示されており、 private package へアクセスするための設定に使われる registriesにアクセスしたい maven repository について追記してそれを参照するようにすれば metadata の取得先に追加される。

github.com

docs.github.com

version:2# ↓を追加registries:maven-google:type: maven-repository
    url:"https://dl.google.com/dl/android/maven2/" # https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/dsl/RepositoryHandler.html#google--updates:- package-ecosystem:"gradle"directory:"/" # ↓を追加registries:- maven-google
    # ...

設定を追記した後の実行ログは以下のような感じ。 maven-metadata.xmlの取得時のアクセス先に https://dl.google.com:443/dl/android/maven2/が増えて、そちらで取得成功しておりバージョン情報の評価ができていることがわかる。

 updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> Checking if androidx.core:core-ktx 1.12.0 needs updating
   proxy | YYYY/MM/DD hh:mm:ss [xxxx] GET https://repo.maven.apache.org:443/maven2/androidx/core/core-ktx/maven-metadata.xml
   proxy | YYYY/MM/DD hh:mm:ss [xxxx] 404 https://repo.maven.apache.org:443/maven2/androidx/core/core-ktx/maven-metadata.xml
   proxy | YYYY/MM/DD hh:mm:ss [xxxx] GET https://dl.google.com:443/dl/android/maven2/androidx/core/core-ktx/maven-metadata.xml
   proxy | YYYY/MM/DD hh:mm:ss [xxxx] 200 https://dl.google.com:443/dl/android/maven2/androidx/core/core-ktx/maven-metadata.xml
 updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> Latest version is 1.12.0
 updater | YYYY/MM/DD hh:mm:ss INFO <job_xxxxx> No update needed for androidx.core:core-ktx 1.12.0

GitHub Actions: repository local な composite action 内で使用している action の更新検知をしたい

Repository 内で composite action を定義している場合、その中で使われている action の更新を検知するには /で設定しているのとは別にそれぞれの composite action に対して設定を書く必要がある。

参考:

qiita.com

version:2updates:- package-ecosystem:"github-actions"directory:"/" # これは .github/workflows/ 以下の yaml が対象になる # ...- package-ecosystem:"github-actions"directory:"/.github/actions/my-action1" # ...- package-ecosystem:"github-actions"directory:"/.github/actions/my-action2" # ...

directoryの指定にワイルドカードを使えるようにする feature request はあるにはあるけど何年も動いてないのであまり期待できなそう:

github.com

おわりに

セットアップまで含めて Renovate 使うのとどっちが楽なんだろ

Klock の今

$
0
0

Kotlin multiplatform 向け datetime ライブラリの Klock は昨年の8月にリリースされた 4.0.10以降リリースされていないように見える。

参考: https://mvnrepository.com/artifact/com.soywiz.korlibs.klock/klock

実際は現在もメンテが継続されており、 korlibs-timeという名前で公開されている。現時点で最新バージョンは 5.4.0

https://mvnrepository.com/artifact/com.soywiz.korge/korlibs-time

これは Klock が含まれる Kotlin 製ゲームエンジン KorGE の 5.0 開発段階で仕切り直し的な状況が発生しライブラリ群の構造が整理されたことによる。そのため 5.0.0 - 5.1.0の間は korge-foundationという全部入りライブラリとして公開されていた。その後また機能毎に module をばらして公開するかということになり現在再び分離中という状況である。 korlibs-timekorlibs-templateと共に 5.2.0から独立した module としてリリースされた。

github.com

また、 Korlibs 系の module 群は project の構成をもっとシンプルにするために別の repository に移すかという計画も進められており、暫くしたらメンテは以下の repository で行われるようになると思われる。

github.com

github.com

余談

"仕切り直し的な状況が発生し"と書いたが、何があったかというと2023年の7月に KorGE の作者が引退宣言をし暫く有志によるメンテナンスモードになるということが起こっていた。

web.archive.org

https://github.com/korlibs/korge/blob/v4.0.10/README.md

In maintenance mode. Soywiz (the founder/primary lead/developer) has retired from maintaining this project (2023/07/27).
Currently this project is only being maintained by a handful of people who may/may not be as familiar with the architecture.
Do not expect any major updates in the future and expect less support if you do decide to use this project.
We are open for pull requests if there are any issues you'd like to fix yourself.

その後作者に心境の変化があったようで9月中頃から開発に復帰し KorGE 5 のロードマップを発表して KorGE 復活、という流れで現在に至る。

web.archive.org

前掲の記事2つは KorGE の公式ブログから削除されているので wayback machineから引っ張ってきている。

Android 13 で getSerializable/getParcelable の API 置き換えが発生して、でも getParcelable にはバグがあって、その後

$
0
0

Android 13 のリリースに関して以下のような Serializable / Parcelableの扱いがしんどいという話があった。この記事ではその後どうなったかについて簡単に記録しておこうと思う。

speakerdeck.com

先に結論を書いておくと、 AndroidX Core に compatible APIが実装されたのでそれらを使えば OK という状況になった*1

スライドの要点

その後

Parcelable向けの対応

https://issuetracker.google.com/issues/242048899を読むと、実はスライド発表時点で Parcelable関係の compatible APIの対応については既に実装されていたことがわかる。ただしその時点では未リリースだったし、 issue の関連付けも行われていなかった*2。この変更は年が明けてすぐの 2023-01-11 に行われた AndroidX Core の 1.10.0-alpha01でリリースされている。

Serializable向けの対応

上記の Parcelableへの対応に関する issue に以下の issue が Serializable向けの feature request としてリンクされており、こちらでトラッキングされるものと思って見ていたが結局使われることはなかった。

その後以下の issue と CL が提出され、 Serializableについてはこちらで話が進められることになった。

この変更は 2024-01-10 に AndroidX Core の 1.13.0-alpha03でリリースされている。なぜか release note に記載はない。

まとめ

  • AndroidX Core の BundleCompat, IntentCompat, ParcelCompatを使おう
    • Parcelable取得の compatible API1.10.0から
    • Serializable取得の compatible API1.13.0から

*1:Serializable に関しては AndroidX Core 1.13.0-alpha03 からなので 2024-03-31 時点で stable は未リリース

*2:NPE バグの報告 issue にはリンクされていた https://issuetracker.google.com/issues/240585930#comment6

Android 15 で edge-to-edge が強制されるかもという話のメモ

$
0
0

何のこと?

www.androidauthority.com

この記事の途中に実装に関する言及があった:

[...], while I was digging through Android 14 QPR2 Beta 3, I discovered a new app compatibility change named EDGE_TO_EDGE_BY_DEFAULT with this description: “make app go edge-to-edge by default if the target SDK is VANILLA_ICE_CREAM or above.” Vanilla Ice Cream happens to be the internal dessert name for Android 15, which means this compatibility change will be applied to apps that target this year’s upcoming release.

じゃあどんな感じか見てみるか、となったので軽く目を通した。

実装とかのリンク

当該の compatibility change のフラグと追加された commit は以下:

あと現時点ではこれ関連だと以下の commit とか mDefaultEdgeToEdgeが使われているあたりを見ておけば十分だろうか:

実装内のコメントを見るに SDK version 35 から入れるつもりでいると捉えて良さそうに思う。

おわりに

本当に Android 15 から有効になるなら、その内以下のページにリストアップされるだろう。とはいえこのレベルの挙動変更なら release note で言及されるとは思う。

developer.android.com

Android: Photo Picker 近況

$
0
0

以下の記事を書いた後くらいから眺めていた photo picker関連の issue についてのメモ。

nashcft.hatenablog.com

Photo picker から返却されるファイルの並び順

issue: https://issuetracker.google.com/issues/264215151

上記の記事でも言及した result で返ってくるファイルの順序が選択順ではない問題。こちらは Google Playの2024年2月のアップデートで修正され、 photo picker 上で選択した順序で返ってくるようになった。

選択ファイル数上限の指定タイミング

issue: https://issuetracker.google.com/issues/303112556

Photo picker で複数のファイルを選択する場合は registerForActivityResultPickMultipleVisualMediaを与えるが、現在選択可能なファイルの上限を指定するにはこの PickMultipleVisualMediaの constructor の maxItemsで指定するため、 launcher を作成する時点で上限が固定される仕組みになっていた。

https://cs.android.com/androidx/platform/frameworks/support/+/a0eabef3092da562c3f9f7b95468b3c368ffee02:activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt;l=826

この issue はこれに対して launcher で実際に photo picker を呼び出す際に上限を指定できるようにしてほしいというもので、以下の CL で対応され、 launch 時に渡すPickVisualMediaRequestを作成する関数に上限数を指定する引数が追加されたものが新たに実装された*1

CL: https://android-review.googlesource.com/c/platform/frameworks/support/+/3106768

この変更によって PickMultipleVisualMediaの constructor と PickVisualMediaRequestの二箇所で上限数を指定できるようになったが、実際に使われるのは2つのうち小さい方になる。

この変更は AndroidX Activity の 1.10.0-alpha01 でリリースとのこと。

Exif情報の取得ができない

issue: https://issuetracker.google.com/issues/243294058

Photo picker 経由で取得したメディアファイルの exif情報が取れなかったり、 MediaStore.setRequireOriginalを使うと permission 不足で SecurityExceptionになるというもの。 MediaStore.ACTION_PICK_IMAGESのドキュメントによると photo picker で取得した URIには様々な制限があるとのこと。

Output: MediaStore content URI(s) of the item(s) that was picked. Unlike other MediaStore URIs, these are referred to as 'picker' URIs and expose a limited set of read-only operations. Specifically, picker URIs can only be opened for read and queried for columns in PickerMediaColumns.

developer.android.com

報告者の Googleの人の会話で位置情報に関しては一応 feature request として受け止められたが、 intended behavior として close されてしまった*2

Close として処理されたが feature request として管理するための issue が作成された様子がないことからか、様々なアプリの開発者たちによる自分のアプリでのユースケースexif情報を読み出せるようにすることの必要性についての投稿が続いていたところ、4月に突然 issue が reopen された。とはいえそこから何か表明があったり話が進んだりしたわけでもなく、状況については不明なままである。

オリジナルのファイル名が取得できない

issue: https://issuetracker.google.com/issues/268079113

Photo picker 経由で取得したメディアファイルのファイル名が数字の連番になっており、元々のファイル名が取得できないというもの。多分 exif情報と同じような理由でマスクしているのだと思うが、こちらは特に対応の期待を感じさせるような話もないまま intended behavior として close された。

Google製のアプリはどうしているのか気になって見てみたところ、 Gmailでの画像添付ではマスクされたファイル名をそのまま使っていた。

所感

ポリシーの変更によって、来年からはアップロードのためのファイル選択のような用途でのメディアファイルへのアクセスには photo picker を使わざるを得なくなるはずなのに、機能面でアプリ開発者と足並み揃えられてないのまあまあ不安に思う。

*1:元々存在した関数の引数に追加せず別に関数を実装したのはバイナリ互換性を保つためとのこと。CL 内の関連する議論

*2:Googleの人の回答が報告者の関心とやや噛み合っていないのも個人的に気になる

Android: 新しいバージョンがリリースされる時の対応の流れに関する備忘録

$
0
0

何度か同じような内容を人に伝える機会に遭遇するなと思ったのでブログ記事にしてしまおう、ということで、 Androidの新しいバージョンのリリースに対してどういう流れで対応を進めるかについての自分の考え方のメモ。

Androidバージョンアップによって発生する変更

以下の3種類に大別できる:

  1. アプリの target API level にかかわらず、当該バージョンで動くデバイス上でアプリを動かす際に関係する変更
  2. アプリの target API level (= target SDK version) をその SDKに合わせた時、当該バージョンで動くデバイス上でアプリを動かす際に関係するもの
  3. SDKAPI変更
    • Android framework の APIの追加・変更・削除
    • Non-SDK interface restrictions の更新

バージョンアップ対応の進め方

まず自分の開発しているアプリへの影響について調査し、必要な対応タスクを上述の3種類に対応した3つのタスクグループとしてまとめる。着手は以下の優先順位で行う:

  1. "Behavior changes: all apps"関連
  2. SDKAPI変更 ->compileSdkを上げる
  3. "Behavior changes: Apps targeting Android XX"関連 ->targetSdkを上げる

優先順位は実質的なデッドラインに当たるイベントに基づいている。つまり、1. に関しては新バージョンの final release, 2. は Jetpackなどのライブラリが compileSdkを上げたリリースを行った時 (final release が行われてしばらく経ったくらい)、そして3. は Google Playの target API level requirements で要求されるようになったら (2024年現在の目安としては final release の翌年の8月末)、ということになる。

Final release 前後で全ての対応を完了できるならそのように進めるのが望ましいが、諸々の事情でもっとかかるみたいな状況になった場合は上記の優先順位とデッドラインをもとにいつからいつまでの間に取り組ませてほしいなど調整をしつつやっていくことになる。 Target SDK version を上げるための対応は素朴にデッドラインに合わせて計画を立てるとスケジュールが間延びしてダレがちなので、リリースされた年内で完了させられるように持っていきたいなと考えている。

なぜこういうことを考えるのか

主にはプロダクトオーナーとかプロジェクトマネージャーとかと計画づくりやスケジュール調整をする時に、どういう対応の段階があってそれぞれの対応が完了すると何がクリアになるかというのを共有するため。あとは何かしらの事情でスケジュール的に余裕がなくて、それぞれの対応に十分な期間や人員を確保しつつビジネス要望にも応えるには何がどの程度まで調整可能かみたいな相談というか交渉というかみたいなのが発生した時に必要になることもある。


Kotlin: non-packed klibs メモ

$
0
0

Kotlin 2.1.0 更新でちょっと躓いたので調べたことの記録として。あまり真面目に調べていないのでちゃんとしてない部分がある。

Non-packed klibs is 何

Kotlin 2.1.0 でサポートされるようになった機能。 What's new での記載は以下:

https://kotlinlang.org/docs/whatsnew21.html#support-for-non-packed-klibs

まずそもそも klibs って何、というのがあるが、これまでの multiplatform module における native target のコンパイル成果物は *.klibというファイルで、このコンパイル生成物のことを指している。ちなみに .klibファイルの実態はただの zip ファイルっぽく、 What's new で言ってる non-packed klibs というのはこの .klibファイルに格納されているもののことのようだ。つまり 2.1.0 から by default での生成物が .klibファイルに圧縮 (= pack) される前の状態になるということらしい。

DroidKaigi 2024 公式アプリの app-ios-shared module を IosArm64 を target にしてコンパイルした時の生成物。左が Kotlin 2.0.21、右が Kotlin 2.1.0 での実行時の結果。

あと成果物を packed にするか unpacked にするかの設定について記載があるけどこれはライブラリ開発者向けっぽい? 今回は結局使い所がなかったのでよく理解していないままでいる。

これ関連で遭遇した問題

ある KMP プロジェクトの Kotlin のバージョンを 2.1.0 に上げた時に、 iOS target でビルドが失敗するようになった。

* What went wrong:
Execution failed for task ':<module>:compileKotlinIosArm64'.
> java.io.FileNotFoundException: /<path to module>/build/classes/kotlin/iosArm64/main/klib/<module> (Is a directory)

エラー内容について簡単に調べてみたら以下のページがヒットした:

https://slack-chats.kotlinlang.org/t/26714754/with-kotlin-2-1-0-new-https-kotlinlang-org-docs-whatsnew21-h

github.com

対象のプロジェクトも moko-resourcesを使用していて、これの更新をしばらくサボっていたこともあってこの件そのものっぽいなーという気持ちで読み進めていたら、 non-packed klib に対応したバージョンに更新しないとダメだよって書いてあったのでハイとなった。 0.24.4で対応されており、更新したら問題が解消された。よかったですね。

github.com

修正内容を見た感じ resources-generator内で klib artifact を扱う時に packed klib しか考慮しておらず、無条件で unzip しようとして失敗していたというのが原因だったようだ。

おしまい

ライブラリの更新をサボってはいけない

個人的 Android project における Gradle まわりの考え方2024年版

$
0
0

Android project で Gradle ファイルをどう書くかについて、現在の自分がどのような考えを持っているかつらつら書いてみる。

きっかけ

随分前の話だけど、以下の投稿から続く Gradle の機能採用に関する判断の考え方を読んで、そういえば自分も以前は同じようなスタンスだったことを思い出した。

それで自分の投稿を探してみると2022年頃にはこんなこと言っていた:

今はこの頃からスタンスが変わっており、以前以下のような投稿をしていた:

現在はこの投稿時からもまた少し変わった、というか当時は仕事で Gradle まわりをの整備を集中的に行っている最中だったこともあって気持ちが強くなっていたのが、時間が経って落ち着いたものになったというか、そんな感じ。

この考え方の変化を思い出してなんだか面白いなと感じたので、ついでに自分のも Gradle に対する考え方の現在のスナップショットを書き出してみるかと思い立ちこれを書くことにした。トピックは折角なのできっかけとなった投稿に倣って「Gradle ファイルを Groovy で書くか Kotlin で書くか」「共通設定をまとめるのに何を使うか」「依存ライブラリの管理方法」の3つにした。なお、この記事の内容は業務で開発するものなど「他人と共有する・他人に引き継ぐ」ことが想定される複数人で開発する Android project を対象としている。個人開発ではみなさん好きにやっていくのがよろしい。

Groovy か Kotlin か

現在は基本的には Kotlin DSLで記述するし、既に Groovy で書かれているものも書き換える方に倒すようにしている。理由は単純で、 project 内で使用する言語のバリエーションを減らしたいからである。2024年時点において Androidアプリはほぼ Kotlin で書かれていることが期待できるので、じゃあ Gradle ファイルも Kotlin で書けた方が (Androidアプリ開発においては) そこでしか使われない Groovy を覚えるより楽だし、Androidアプリ開発初心者や Groovy/Gradle に慣れ親しんでない人は細かい文法の差異で混乱することがなくなって嬉しいよね、という発想。

ただ、絶対に Kotlin DSLにするというほど気持ちがあるわけではない。私自身は Gradle ファイルが Groovy で書かれていて困ることはないので、例えば Kotlin DSLが出る前から開発されているアプリでチームメンバーも古くからいる人たちがずっと残ってて、 Groovy で書かれていることに課題を感じていないみたいな状況だったら Kotlin DSLに書き換えようとはしないと思う。 Groovy 触ったことないって人が加入したらどうしようかって話を始める感じ。

そこまで気持ちが強くないのは書き換えによる体験の差がそこまで大きくないから、という印象にもとづいているように感じる。静的型付きの言語で書くようになったからと言ってコード補完が速くなるわけでもないし*1、私がこれまで触れてきたアプリの規模や構成だとビルド実行時間がわかりやすく短くなるわけでもなかった、といったような実利面の弱さをこれまでの経験から所感として持っている。なので自分にとっての書き換えのモチベーションは、繰り返しになるがあまりビルドスクリプトに触れる機会のない人にとっても負担が少なくなるようにアプリと同じ言語で書けるようにしようというメンテナンスへの配慮が殆どとなる。

共通な設定のまとめ方 (script plugins, pre-compiled plugins, binary plugins)

真面目に multi-module 化をして、数十 module にはなるだろうなという見立てがある場合には build-logic module を作成して binary plugins で共通設定を記述する、というか本当にちょっとしたプロダクトでない限り仕事で開発するようなアプリはそのようになるだろうと考えているので基本 binary plugins 派になったと言えるか。「本当にちょっとした」の自分の中での基準は分割したところで十個も行かないくらいで、このような規模の小さい project の場合は script plugins で済ませてしまうかもしれないし、そもそも記述をまとめず重複を受け入れてしまうかもしれない。

Project の規模以外の判断基準だと build-logic module の用意や Gradle plugin の書き方を覚える (覚えてもらう) ための手間とビルドパフォーマンスのトレードオフかなーという感じ。とはいえビルドパフォーマンスの方が大事でしょというのと今は nowinandroidDroidKaigi 公式アプリみたいな参考実装があるという点から迷ったら binary plugins を書く方に倒しがち。

きっかけで書いたように元々全部 script plugins でいいじゃんと考えてたところから変化したのは、おもに以下の記事・スライドから影響を受けたため:

依存プラグインやライブラリの定義・管理方法

これのスタンスに関しては自分はかなり適当で、文字列リテラルで何度も同じものを書く羽目にならなければ別に何でも構わないと思っている。他は強いて言うなら Renovate や Dependabot みたいなので更新を自動化できることくらいだろうか。

ここのところは Gradle が公式で出している仕組みだからということで version catalog を入れるようにしているが、どちらかというと type-safe project accessorsは絶対使うのでそれと構文を合わせるために使っているという面が強い気がする。

Version catalog と言えば以前は呼び出し側からのコードジャンプで生成コードに飛ばされるなど不便に感じることもあったが、最近は IDEのサポートも充実しており特使用感で気になることは無くなってきた。もしくは単に慣れたか。

考え方の変化について

変化については大雑把にはふたつ理由があって、ひとつはここ2年ほどビルドスクリプトまわりを自由にさせてもらえている*2ことでそれなりの知識と経験を得られたのが大きかったと思う。これによって自分の中でのコントロール可能な要素が広がり、作り込み度合いの段階ややりすぎ感の出てくる程度の肌感を得ることができた。また、上で挙げた repository や資料といった参考として紹介できるものの把握や評価ができたのも、こちら側に倒して良いかと考える後押しになった。

もうひとつは今いる組織の性質的に新しい仕組みや機能を導入する傾向が強く、保守的な対応の方が穏便だと考えていても割と先進的な選択をとりがちな状況に置かれているという事情がある。結局のところ保守的な対応方針を取るにはそれ相応に古くからの深い知識や長い付き合い (ただし漫然としたものではない) による匙加減の把握といったものが必要で、それらがあって初めて必要最低限の対応や素朴な構成の維持が可能になると考えている。そういった Gradle 熟練者のいない、つまりはこれから Gradle について学んでいくことになる組織では、割と無邪気に新機能を放り込まれたり、かなり大味な設定変更で異様に先進的な設定になったりすることがままある (あった)。それなら昔ながらのやり方に押し込むよりは先回りして現代的な構成をとっていた方が変更に対するコントロールがしやすく、かつ他チームメンバーのメンテや習得のモチベーションを損なわずにやっていけるだろうという気持ちになり、じゃあもうどんどん先行していくかとガチャガチャやっている内に考え方もそっち寄りになったように思う。

こんな感じで割と現代的な機能を採用するのにポジティブな考え方になってはいるが、上の3トピックに対するスタンスでも書いているように絶対そうするというほどの気持ちの強さがあるわけではないのは、そうするように説得したり、既存のものを書き換えたりするほどの特別なメリットがあるとは自分自身は感じていないからだろう。なので、きっかけにある TimeTree のようにすでに体制があって、それで上手くやってきているところでは自分も態々コストかけて書き換えたりはしないだろうなあとは思う。自分は今のところ諸々含めて「これからやっていく」という文脈の中にいるので、じゃあ .ktsで書く方が Gradle に慣れていない人にもなじみやすいだろう、これから multi-module 化を本格的にやっていくなら下準備として build-logic module で binary plugins として共通部分を括り出しておくと module 増やすの楽だし将来パフォーマンス面も苦しくなりにくいだろう、とか、そういう判断をして良いのでそうしているという感じ。

おわりに

Gradle まわりの扱いについて、現時点での考え方と、以前の素朴に書いていれば良いという考え方からどのように変わったのかについて書いた。自分は他の人がどういうことを考えているのか知れるのが割と嬉しいのできっかけの投稿や記事も含めてこういう文章は好きなのだが、今回の場合何か主張があって書いたわけではないので全体的に取り留めのないものになってしまった。まあこういった話題には固定的な「答え」のようなものは無く、それぞれの状況があるばかりなのでそれに応じて上手くやっていきましょう、皆さんはどうですか? という気持ちだったのかもしれない。

9月末ぐらいにぼんやりと書き始めてから中々まとまらず結局年末ギリギリになってしまったが何とか2024年内に書き終えられてよかった。

*1:これは自分が Gradle ビルドスクリプトの基本的な APIをある程度把握しているから迷わずタイプできるという点も影響しているかもしれない。たとえば自分で何かしらの property や関数の呼び出しを書き切る方が補完サジェストが表示されるより早い、みたいなことがよくある。

*2:というか最初はとあるビルド構成まわりが廃墟寸前の project を全部いい感じにする or die みたいな状況だった

Viewing all 97 articles
Browse latest View live