Xamarin.AndroidXMigrationについて

今回はコレに関連して少し詳しく書く。

devblogs.microsoft.com

目次

Xamarin.Androidは時代遅れ

Xamarin.AndroidとスタンダードなKotlin中心のAndroid開発の乖離は年々ひどくなっている。たとえば

  • data-binding, room: Android Gradle Pluginの中で行われるコード生成を前提としているので難しい。
  • constraint layout designer: constraint layoutはそれ自体はNuGetパッケージで配布されていて、実行時に必要なライブラリとしては使うことができるが、テキストエディタ上で書くには冗長で区別の付けにくい属性が多用されていて、UIデザイナーが無いと使いにくい機能の一つだ。
  • navigation editor: これも実行時ライブラリとしては単にバインドするだけだが、GUIデザイナーで流れを把握できるのと出来ないのとでは大きな違いがある。
  • instant run, apply changes: Xamarin.Androidにはinstant runライクなfast deploymentが実装されているが、デフォルトで有効になっている機能としては、あくまでDLLのみの変更にしか対応していない。Java生成コードの変更を伴うimproved fast deploymentサポートは無効のままだし、その間にAndroid Studioはinstant runを捨ててApply Changesに移行してしまった。

最後に、Kotlin + Android Studioでの開発においては、AndroidサポートライブラリからJetpackへの移行が進んでいる。次の節ではこれについてもう少し詳しく説明しよう。

androidx (a.k.a Jetpack)

Androidサポートライブラリは、Android APIフレームワークレベルで機能追加されたもののうち、特に新しいハードウェア要件などに依存しない部分を、古いバージョンのAndroidでも利用できるようにバックポートしたものだ。android.support.v4.*というパッケージはminSdkVersion=4であるような追加APIが、android.support.v13.*というパッケージならminSdkVersion=13であるような追加APIが、それぞれ含まれていた(他にもv7, v17などいくつか存在した)。

これらのバージョン番号に基づく識別子は、最初は正しかったが、長い年月を経て、Google PlayAndroid API Level 4のサポートが打ち切られたりする中、だんだん意味をなさなくなってきた。またAndroid Architecture Componentはandroid.arch.*というパッケージ名だったが、将来的にはこれらとサポートライブラリの関係なども錯綜する可能性が高く、新機能がどちらに属するのかといった疑問など、トラブルの予感しかなかったといえる。

Googleは2018年にJetpackという新しいブランディングandroidx.*というパッケージ名のもとで、古いサポートライブラリとアーキテクチャコンポーネントを統合した。そしてminSdkVersionは各パッケージごとに決まる事項となった。Androidフレームワークに追加する必要がない新機能はJetpackでサポートすれば良く、しかもフレームワークのリリースサイクルからは独立して基盤的な新機能をリリースできるようになった。

また、Jetpackの大きな側面は、Javaが前提だった頃に作られたライブラリを、Kotlin前提の構成に置き換える、ということにもある。Xamarin.Androidの文脈では「ネイティブ」としてKotlinよりも(C++よりも)Javaのほうが出てくるが、これはmonoランタイムとの相互運用の文脈で出てくるのがほとんどなので、今後も変わらないだろう。これ自体は別にXamarinがKotlin前提のエコシステムに追いついていないということは全く意味しない。

androidxへの移行の課題

さてAndroid開発者はこのJetpackへ移行することが求められているのだけど、これは簡単にbuild.gradleの中にあるパッケージを差し替えるだけでできるようなことではない。パッケージ名が変わっているので、ソースコードも書き換えなければならない。そしてJetpackパッケージは膨大である。 https://developer.android.com/jetpack/androidx/migrate にリストがあるので見てみてほしい。

これはとても全部追っかけていられない

仮に膨大な肉体労働の後に自分が書いたアプリケーションのコードを移行できたとしても、それだけでは十分ではない。自分がアプリケーションで参照しているライブラリがJetpackに移行していない場合は、結局古いサポートライブラリが含まれてしまうことになる。

運の悪いことに、Android Studioのデフォルトテンプレートで作成されたアプリケーションやライブラリは、古いバージョンのAndroidでも動作できるようにandroid.support.v4*の各種パッケージに含まれるクラスが使われていた。

依存ライブラリがバイナリとして参照しているものを、そのユーザー開発者が書き換えるのは無理がある。依存ライブラリがandroidxに移行してくれるのを待つべきだろうか? そもそもそのライブラリの開発者は今でもそのライブラリをメンテしているだろうか? そしてタイムリーに対応してくれるだろうか? 依存ライブラリの開発者が1人でも「まだ…その時ではない」と考えていたら、移行計画が実現することはないだろう。

Jetifier

この問題を解決するためにGoogleが出してきたのがJetifierという自動移行ツールである。Jetifierは単独実行可能なツールとしても存在するが、Android Gradle Pluginでサポートされているので、通常は次のような記述をbuild.gradleに含めて使用することになるだろう(今回はGradleの話ではないので説明は最小限にとどめる)。

  • android.useAndroidX=true
  • android.enableJetifier=true

Jetifierは内部的に次のようなことをやっている。

  • コード中に含まれるandroid.support.*への参照をandroidx.*に変換する
    • ただしパッケージ名だけの変換になるとは限らない
  • binary to binary compiler (jar/aar/zip, ソースではやらない)
  • Java, XML, POM, proguard configs (がaarに含まれている)

ちなみに単独実行ツールでは逆向きにも変換できる… jetifier-standalone -r -i input.aar -o output.aar

ちなみに、移行対象のアプリ中にandroid.support.xyzとandroidx.xyzが混在していても問題なく動作することになっている。移行作業は段階的に行われるはずなので、これが求められている挙動であるといえる。

また、Googleの公式ライブラリ以外でandroid.support.xyzというパッケージが含まれている場合もandroidxに置き換える対象となるようだ。この場合はパッケージ名以外で何を置き換えるのが正しいのかツール側にはわからないので、パッケージ名だけが置き換わる。

いずれにしろ、Jetifierについて改めて強調しておくべきことがひとつある。Jetifierは移行を簡単にするためのツールではなく、移行を実現するためにほぼ必須となるツールだということだ。アプリケーション開発者にはバイナリ参照と手作業で書き換える適切な手段が無いのである。

Xamarin.AndroidJetpack移行戦略

Jetpackへの移行は、もちろんXamarin.Androidについても他人事ではない。Xamarin関係のライブラリの多くはAndroidサポートライブラリに依存している。それはVisual Studio for Mac / for WindowsのテンプレートでもAndroid.Support.v4への参照がデフォルトで含まれていたり、Xamarin.FormsであればそもそもAndroidプラットフォームバインディングXamarin.Forms.Platform.Android)がそうなっていたからだ。

Xamarin.Androidの場合、問題はJetpackよりも悪化している。AndroidバインディングとはDLLであり、NuGetパッケージに含まれるバイナリである。そしてソースコードが無いことも少なくはなく、これを自前で何とかするのは無理がある。

Jetpackへの移行は、本家がアプリケーション開発者の独力では対応できないと判断したタスクなのだから、Xamarin.Androidでも自動対応ツールが必要であると考えるのが妥当だ。そういうわけで、Xamarinからは新しくXamarin.AndroidxMigrationというパッケージが爆誕した。

github.com

JetifierはAndroid Gradle Pluginのタスクとして対応したので、ユーザーはgradle.propertiesに2行追加するだけで済んだ。Xamarinというか.NETの場合は、Gradle Pluginのようなことを実現するためには、MSBuildタスクを含むNuGetパッケージを追加して、ビルドをinjectするかたちで対応する。

ちなみに、Xamarin.Androidのandroidxのバインディングは、2019年7月になってNuGet上に出現するようになった。 https://www.nuget.org/packages?q=xamarin.androidx で検索してみるとよい。ソースコードhttps://github.com/xamarin/AndroidSupportComponents/tree/AndroidX (AndroidXブランチ)にある。ここにはroomなども含まれているが、あくまでライブラリのバインディングに過ぎず、Java/KotlinであればAndroid Gradle Pluginによって利用できていたroomがXamarinで使えるようになったわけではない。

Xamarin.AndroidXMigrationの仕組み

Xamarin.Androidの仕組みについて把握している人なら知っていると思うが、Xamarin.Androidアプリケーションでは、アプリケーション中のDLLで使われているJava派生クラス(Managed Callable Wrapper)からJNIで呼び出されるJavaクラスを含むjarが生成されてパッケージされる。このJarを他の参照ライブラリに含まれるJarと合わせてJetifierの対象として変換を行う。

そしてDLL中の旧ライブラリへの参照をXamarin.AndroidXへの参照に置き換える。Jetifierが行っているようなバイナリ書き換えを、DLLについても行っていることになる。バイナリ書き換えはcecilでやっている。

NuGetパッケージを追加するだけでこの変換機構が自動的に動作する仕組みは、NuGetのMSBuildタスク拡張機能を利用している。これについては日本語ではここで詳しくまとめられているようだ。

www.kekyo.net

ただし、適用されるMSBuildタスク拡張の順番は制御できないので、これはやっつけ仕事に類するものだ(23:30追記: 別のパッケージが、関連するTargetにTargetDependsOnで参照アセンブリのリストやそのアセンブリ群に手を加えた場合のことを考えるとよい)。ユーザーとしてはビルドの「最後」に適用されることを祈るか、そうなるように各プロジェクトファイルをいじることになるかもしれない。(ちなみにGoogleAndroid Gradle Pluginは拡張タスクではなくプロパティで実現しているので、「正しい」雰囲気がある。)

単独ツールとしては androidx-migrator.exe でも実行可能だ。そして、Xamarin.AndroidXMigration.ToolというNuGetパッケージがdotnet toolとして作られている。これは新しいMSBuildの記述スタイルであれば Sdk="Microsoft.NET.Sdk"<IsTool>true</IsTool><PackAsTool>true</PackAsTool> の組み合わせだけで作れる(パッケージがそうやって作られているという意味であって、ユーザーはこう記述する必要はない)。

Xamarin.AndroidXMigrationの注意点

公式ブログにも書かれているが、Xamarin.AndroidXMigrationを適用したアプリケーションには、Xamarin.Android.Support.*Android.Arch.*への参照を(間接的にでも)残しておかなければならない。AndroidXMigrationはアプリケーションをいったん古いコードでビルドすることが前提になっているし、こうしておかないと、AndroidXMigrationが移行元のアセンブリとしてのXamarin.Android.Support.*をロードできなくなってしまうためだ。

その他

androidxへの移行は他のフレームワークでも問題になる。たとえばReact Nativeでも同様のjetifierツールが開発されているようだ。Flutterでも1.7リリースの時にAndroidXへのサポートは作業中である、と説明している(1.9リリースノートにはJetpackやJetifierへの言及が無い)。