仕事でちょっと困ったので、調査ログを残しておく。
長い長い前提知識
そもそもAndroidXとはなんなのか、Jetifierとはなんなのかを記しておく。
AndroidXの成り立ち
前提知識として、Android開発における最も一般的な外部ライブラリとして Android Support Library / AndroidX
というものが存在する。
もともとAndroidには Android Support Library
というものがあって、これの後述する問題を解消したものが AndroidX
である。
どの程度一般的かというと、Android Studioで新規にプロジェクトを作成するとそもそも標準で入ってくるし、Googleの公開している (Googleのビジネスに依存した) Android向けライブラリでは当然のごとく依存関係に含まれている。
というのも、Support LibraryしかりAndroidXが目指しているのは「Android OSから独立した、Androidのリリース全体に渡る下位互換性の提供」であり、アプリが実行されるOSバージョン自体がどんなバージョンであってもつつがなく動作することを保証するためのライブラリであるからだ。
すなわち特定バージョンのAndroidリリースと同期的に変更されるアプリ、具体的にはOS標準のシステムアプリなどでない限り、アプリはネイティブのAPIを直接叩くのではなく、Support LibraryしかりAndroidXのAPIを利用すべきである。
冒頭の「なぜAndroidXが登場したのか」という部分だが、これは
- 最低サポートバージョンごとにモジュールが分離されており、ファットになっていた
- モジュール間が密に結合していることもあり、想定バージョンごとにmajorバージョンをまとめて全部上げる方針になっていた
などのSupport Libraryの問題を解消すべく、
- 機能ごとにモジュールを分離した
- モジュールごとに独立したsemverを採用した
をはじめとする複数の修正を行いイチから再スタートしたのがAndroidX、ということになる。
Jetifierとはなにか
当然ながら、AndroidXはこれから更新が重ねられていく。すなわち新たな機能を利用したい場合、AndroidXへの移行を余儀なくされる。
しかしながらメンテナンスの停止したライブラリを利用しているだとか、アプリ自身が独立してAndroidXの機能をいち早く利用したいだとかの場合、
- Support Libraryへ依存しているライブラリ実装
- AndroidXへ依存しているライブラリ実装
- AndroidXへ依存しているアプリ実装
などが混在してしまう可能性がある。
これを解決するため、GoogleがJetifierというツールを提供している。これを用いると、Support Library v28を利用していることを前提にそれらのクラス参照たちをAndroidXのものへ繋ぎ変えてくれる。スタンドアロン版を直接利用することも可能だが、ビルドスクリプトでフラグを立てるだけで自動で実行させることができる。
AndroidX対応はいつやるのか
本題。これらの前提のもと、じゃあいつ自分たちの作っているアプリでAndroidXに対応するのか、という話がいつかは出てくる。
実際自分の関わっているいくつかの事業者を見ていると、AndroidXへの本格対応は未だ行っていないケースが散見される。
いくらでも例外はあるものの、筆者としては「早めにやるに越したことはないが、最悪AndroidXの機能が必要になるまではJetifier以上の対応はしなくてよい」という意見を推していきたい。
この「AndroidXの機能が必要になるまで」に外的要因も含まれうるという点で、いくらかの危険性をはらんでいることも無視できないのが難しい部分だ。
Android Pluginの挙動
下記のようなアプリがあるとする。このアプリはSupport Library v28の機能に依存していて、具体的には古いAndroid OSにおけるbackgroundTint
の実現にAppCompatを利用している。
この機能の実現にはAndroidXでの更新以降の機能は要求しておらず、Support Libraryとして提供されていた期間までの機能で必要十分である。
github.com
この時点での依存関係ツリー
./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
このアプリでandroid.useAndroidX
をenableにしてみる。
diff --git a/gradle.properties b/gradle.properties
index 8964a61..f96c140 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m
kotlin.code.style=official
-android.useAndroidX=false
+android.useAndroidX=true
android.enableJetifier=false
この時点ではアプリ側のコードを書き換えずビルドが可能である。それもそのはず、依存関係の書き換えは行われないのだ。
このdiffにおける依存関係
./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
ではこのフラグはどのような影響を与えるのか?答えはAndroid Developersの記述が全てだ。
android.useAndroidX: true に設定すると、Android プラグインは Support Library ではなく、該当する AndroidX ライブラリを使用します。設定しない場合、このフラグはデフォルトで false です。
Android Pluginが当該ライブラリを利用する箇所としてわかりやすいものには、DataBindingが存在する。たとえば下記のようなDataBindingを用いたアプリの場合。
github.com
この時点でのdependencies
./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
useAndroidX
をenabledにしてみる。
diff --git a/gradle.properties b/gradle.properties
index 8964a61..f96c140 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m
kotlin.code.style=official
-android.useAndroidX=false
+android.useAndroidX=true
android.enableJetifier=false
この状態でビルドを試みると失敗することが確認できるだろう。
./gradlew :app:buildDebug --console=plain
この時点のdependencies
./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
内部で利用されるライブラリがAndroidXの置き換わるとは、依存関係に下記のような差分が発生することを指しているのである。
10,51c10,23
< +--- com.android.databinding:baseLibrary:3.5.0
< +--- com.android.databinding:library:3.5.0
< | +--- android.arch.lifecycle:runtime:1.0.3 -> 1.1.1
< | | +--- android.arch.lifecycle:common:1.1.1
< | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< | | +--- android.arch.core:common:1.1.1
< | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< | +--- com.android.support:support-core-utils:26.1.0 -> 28.0.0
< | | +--- com.android.support:support-annotations:28.0.0
< | | +--- com.android.support:support-compat:28.0.0
< | | | +--- com.android.support:support-annotations:28.0.0
< | | | +--- com.android.support:collections:28.0.0
< | | | | \--- com.android.support:support-annotations:28.0.0
< | | | +--- android.arch.lifecycle:runtime:1.1.1 (*)
< | | | \--- com.android.support:versionedparcelable:28.0.0
< | | | +--- com.android.support:support-annotations:28.0.0
< | | | \--- com.android.support:collections:28.0.0 (*)
< | | +--- com.android.support:documentfile:28.0.0
< | | | \--- com.android.support:support-annotations:28.0.0
< | | +--- com.android.support:loader:28.0.0
< | | | +--- com.android.support:support-annotations:28.0.0
< | | | +--- com.android.support:support-compat:28.0.0 (*)
< | | | +--- android.arch.lifecycle:livedata:1.1.1
< | | | | +--- android.arch.core:runtime:1.1.1
< | | | | | +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< | | | | | \--- android.arch.core:common:1.1.1 (*)
< | | | | +--- android.arch.lifecycle:livedata-core:1.1.1
< | | | | | +--- android.arch.lifecycle:common:1.1.1 (*)
< | | | | | +--- android.arch.core:common:1.1.1 (*)
< | | | | | \--- android.arch.core:runtime:1.1.1 (*)
< | | | | \--- android.arch.core:common:1.1.1 (*)
< | | | \--- android.arch.lifecycle:viewmodel:1.1.1
< | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
< | | +--- com.android.support:localbroadcastmanager:28.0.0
< | | | \--- com.android.support:support-annotations:28.0.0
< | | \--- com.android.support:print:28.0.0
< | | \--- com.android.support:support-annotations:28.0.0
< | \--- com.android.databinding:baseLibrary:3.5.0
< +--- com.android.databinding:adapters:3.5.0
< | +--- com.android.databinding:baseLibrary:3.5.0
< | \--- com.android.databinding:library:3.5.0 (*)
---
> +--- androidx.databinding:databinding-common:3.5.0
> +--- androidx.databinding:databinding-runtime:3.5.0
> | +--- androidx.lifecycle:lifecycle-runtime:2.0.0
> | | +--- androidx.lifecycle:lifecycle-common:2.0.0
> | | | \--- androidx.annotation:annotation:1.0.0
> | | +--- androidx.arch.core:core-common:2.0.0
> | | | \--- androidx.annotation:annotation:1.0.0
> | | \--- androidx.annotation:annotation:1.0.0
> | +--- androidx.collection:collection:1.0.0
> | | \--- androidx.annotation:annotation:1.0.0
> | \--- androidx.databinding:databinding-common:3.5.0
> +--- androidx.databinding:databinding-adapters:3.5.0
> | +--- androidx.databinding:databinding-common:3.5.0
> | \--- androidx.databinding:databinding-runtime:3.5.0 (*)
58c30,42
< +--- com.android.support:support-compat:28.0.0 (*)
---
> +--- com.android.support:support-compat:28.0.0
> | +--- com.android.support:support-annotations:28.0.0
> | +--- com.android.support:collections:28.0.0
> | | \--- com.android.support:support-annotations:28.0.0
> | +--- android.arch.lifecycle:runtime:1.1.1
> | | +--- android.arch.lifecycle:common:1.1.1
> | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
> | | +--- android.arch.core:common:1.1.1
> | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
> | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
> | \--- com.android.support:versionedparcelable:28.0.0
> | +--- com.android.support:support-annotations:28.0.0
> | \--- com.android.support:collections:28.0.0 (*)
62c46,68
< +--- com.android.support:support-core-utils:28.0.0 (*)
---
> +--- com.android.support:support-core-utils:28.0.0
> | +--- com.android.support:support-annotations:28.0.0
> | +--- com.android.support:support-compat:28.0.0 (*)
> | +--- com.android.support:documentfile:28.0.0
> | | \--- com.android.support:support-annotations:28.0.0
> | +--- com.android.support:loader:28.0.0
> | | +--- com.android.support:support-annotations:28.0.0
> | | +--- com.android.support:support-compat:28.0.0 (*)
> | | +--- android.arch.lifecycle:livedata:1.1.1
> | | | +--- android.arch.core:runtime:1.1.1
> | | | | +--- com.android.support:support-annotations:26.1.0 -> 28.0.0
> | | | | \--- android.arch.core:common:1.1.1 (*)
> | | | +--- android.arch.lifecycle:livedata-core:1.1.1
> | | | | +--- android.arch.lifecycle:common:1.1.1 (*)
> | | | | +--- android.arch.core:common:1.1.1 (*)
> | | | | \--- android.arch.core:runtime:1.1.1 (*)
> | | | \--- android.arch.core:common:1.1.1 (*)
> | | \--- android.arch.lifecycle:viewmodel:1.1.1
> | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
> | +--- com.android.support:localbroadcastmanager:28.0.0
> | | \--- com.android.support:support-annotations:28.0.0
> | \--- com.android.support:print:28.0.0
> | \--- com.android.support:support-annotations:28.0.0
113c119
< BUILD SUCCESSFUL in 0s
---
> BUILD SUCCESSFUL in 1s
Jetifierの挙動
android.enableJetifier
による影響はもう少しわかりやすい。たとえば最初に挙げたSupport Libraryによるアプリで有効にしてみる。
diff --git a/gradle.properties b/gradle.properties
index 8964a61..b5dd627 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx1536m
kotlin.code.style=official
-android.useAndroidX=false
-android.enableJetifier=false
+android.useAndroidX=true
+android.enableJetifier=true
これにより、依存関係ツリーが書き換わる。
この時点でのdependencies
./gradlew :app:dependencies --configuration=debugRuntimeClasspath --console=plain
\--- com.android.support:appcompat-v7:28.0.0 -> androidx.appcompat:appcompat:1.0.0
ツリー内のこの箇所にあるとおり、アプリ自身で参照していたSupport LibraryがAndroidXのものに置換されるのである。
しかしプロジェクト内のソースコードや出力されるバイトコードそのものには影響が出ないことには注意が必要だ。すなわち、コードの書換えは必要である。
./gradlew :app:buildDebug --console=plain
さて、では依存するライブラリではどうだろうか。たとえばSupport Libraryを依存関係に持つライブラリを追加する。Jetifierを有効にしていない場合は下記のような依存関係が出力される。
\--- jp.s64.android.toolbox:supportnotificatonkt:1.3.0
+--- org.jetbrains.kotlin:kotlin-stdlib:1.3.31 -> 1.3.50 (*)
\--- com.android.support:support-compat:28.0.0 (*)
この時、この依存するライブラリをデコンパイルすると下記のようなものが確認できるだろう。
package jp.s64.android.toolbox.support.notification
@android.support.annotation.RequiresApi public fun android.support.v4.app.NotificationManagerCompat.createNotificationChannel(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Unit { }
public fun android.support.v4.app.NotificationManagerCompat.createNotificationChannelOrNothing(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Boolean { }
public fun android.support.v4.app.NotificationManagerCompat.getNotificationManager(context: android.content.Context): android.app.NotificationManager { }
public fun android.support.v4.app.NotificationManagerCompat.isChannelSupported(): kotlin.Boolean { }
ではJetifierを有効にしてみる。当然ネストした依存関係も適切に処理され、下記のようになる。
\--- jp.s64.android.toolbox:supportnotificatonkt:1.3.0
+--- org.jetbrains.kotlin:kotlin-stdlib:1.3.31 -> 1.3.50 (*)
\--- androidx.core:core:1.0.0 (*)
さきほど検証したとおり、自身のプロジェクトにおけるソースコードは書き換わらない。しかしこの依存ライブラリが持つコードをデコンパイルすると、下記のようなものが確認できる。
package jp.s64.android.toolbox.support.notification
@androidx.annotation.RequiresApi public fun androidx.core.app.NotificationManagerCompat.createNotificationChannel(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Unit { }
public fun androidx.core.app.NotificationManagerCompat.createNotificationChannelOrNothing(context: android.content.Context, channel: android.app.NotificationChannel): kotlin.Boolean { }
public fun androidx.core.app.NotificationManagerCompat.getNotificationManager(context: android.content.Context): android.app.NotificationManager { }
public fun androidx.core.app.NotificationManagerCompat.isChannelSupported(): kotlin.Boolean { }
ライブラリ内での参照は自動で書き換わることが確認できた。
アプリにおける基本的な対応
これらを踏まえ対応におけるポイントをまとめると、
- Support Library 28まで上げておく
useAndroidX
, enableJetifier
を有効にする
- 壊れたプロジェクト内の参照を修正する
- 依存するライブラリは自動で書き換わるので気にしなくてよい
の4点を押さえれば十分ということになる。実に簡単だ。
対応の判断
対応は簡単だが、問題はこの判断を行うタイミングや要因である。
- AndroidX以降の更新における機能がアプリにおいて必要になった
- AndroidX以降の更新におけるBugfix等がアプリに影響があると判明した
- 依存するライブラリがSupport LibraryではなくAndroidXを利用する形へ移行した
1は完全にプロジェクトのコントローラブルな範囲であるため割愛する。
2に関しては一般的なライブラリにおける破壊的変更が実施された以降とほぼ同じと捉えてよい。
3はほぼ2と同じ事由になることが想像されるが、少し問題があるため後述する。
これらを総合して言えることは、やはり「やれる内に更新をしていないと工数の確保が難しくなる」という点は他のライブラリと変わらないということだ。
もしアプリのロジックやビジネスが特定ライブラリに密に依存していて、それらのアップデートが必要となった際に雪だるま式にやることが増えてしまうかもしれない。
非アプリにおける対応
では非アプリ、具体的には複数アプリに導入されうるモジュールの場合にはどうだろうか?対応の判断面は変わらない。要は「機能が必要になった」「依存するライブラリが移行した」場合に対応すればよい、ということだ。
しかし「早めに更新したほうがよいか」というと、そんなことはない。むしろ「できる限り対応を遅めたほうがよい」と筆者は考える。
AndroidXとSupport Libraryの間には名前空間やモジュールの分割といった変更があり、大きな変更のように思わせる。
しかし実際には、アプリ側でJetifierを用いることでSupport Libraryからの移行は容易に行える。
では逆にAndroidXに対応したものをSupport Libraryを前提としたアプリで利用するのが容易かというと、これは難しい。JetifierはSupport Libraryを用いた参照をAndroidXのものに変換する機能こそ利用できるが、その逆は困難だからだ。
AndroidXへの対応は簡単だが、それは十分にそのプロジェクトへアサインされたエンジニアのリソースが存在する場合に限られる。現実問題として、必ずしもプロジェクトにフルタイマーや十分な能力を持ったメンバーがアサインされている保証はない。
複数のアプリで利用されるモジュールがAndroidXの利用を要求するようになった場合、必然的にこのモジュールを利用している全てのアプリは移行の作業を行うこととなる。
「ではAndroidX対応しなくてもいいのか?」と聞かれそうになる。この答えはYesだ。なぜならSupport Libraryさえ使っていれば、実際にどちらを利用するかはアプリ側に判断を委ねることができるからだ。Jetifierを有効にするだけでよい。
まとめ
長々と書いてきたが、この記事における主張は下記のとおりだ。
- アプリにおけるAndroidX対応は、最悪必要になるまで対応しないという選択が可能である。が、外的要因に影響されないよう早めにやればよいし、通常は依存するライブラリに関わらず今からでも対応可能である。
- 非アプリにおける対応は、可能な限り遅めたほうがよい。Support Libraryを使っている限りは実際のアプリに判断を委ねることができる。
筆者は業務でエンドユーザ向けアプリからSDKまで様々な開発に携わっているが、AndroidXに関する対応は事業者によってまちまちである。
しかしながらエンジニアを対象としたプロダクトを作る事業者においては、ユーザであるエンジニアの利便性を損なわない対応を求めたいものだ。
おまけ: 実はde-jetifierは存在する
AndroidXからSupport Libraryの利用へ戻すのは困難と記述したが、困難ではあっても可能ではある。
JetifierはAndroid Gradle Pluginと統合されているが、CLIからも利用ができる。そしてこのCLI版には Reverse mode
が存在している。
developer.android.com
依存先ライブラリがAndroidXへ移行してしまったが、実態としてAndroidXの機能を利用していない (Support Library時代に提供されていた機能の範囲に留まっている) 場合は、これを利用するのもひとつの手段かもしれない。