飴屋

Kotlin/Composableに移行する

何をするのか

先日、Android15が配信され、それに伴ってEdge-to-Edgeなる新しめな施策が強制オンとなって、アプリにちょっと手を入れていたのですが、どうも旧来のレイアウトシステムではEdge-to-Edgeの恩恵が得られないっぽいし、そもそも旧来のレイアウトシステムの情報がググっても探しにくくなっているっぽくて、いよいよこのままだとアプリが遺物になっていくぞと危機感を覚えた次第です。危機感を覚えないと動かないのもダメなんですが、ここで動かないよりはマシということで、重たい腰をあげることにしました。

やりたいことは旧来のレイアウトシステムをComposableなレイアウトに置き換えることであるとわかってはいるんですが、レイアウトの仕組みをそもそも正確に把握していないので、何をすればいいのかを順序だてて考えてみましょう。

setContentViewを捨てたい

まず旧来のレイアウトシステムは、layoutリソースの中でxmlファイルとして定義されたレイアウトをsetContentViewで読み込んで実現されていました。XMLが超流行った時代がありまして、もう全てのドキュメントがXMLになっちゃうんじゃない?っていう勢いがあったんですが、最近会話の中でXMLって単語を聞かない私界隈です。WordとかExcelのファイルをZip展開するといっぱい出てくるかもですね。(今だとみんなJSON?)XMLで静的に定義するやり方は、あんまり私には馴染めなかったので、このレイアウトXMLファイルの中身を、ktソースファイルに移動していけばよさそうにみえます。全て移行し終えたら不要になったsetContentView命令ごとレイアウトXMLを捨てるというのが当面の目標です。もうandroid:idみたいな長ったらしいプロパティ名を書かなくてよくなるのかな。

新旧で見比べる

私のアプリのレイアウトってほぼほぼAndroid Studioを使って新規に立ち上げたプロジェクト雛形から多く変わることはないので、古いアプリと比較的最近作ったアプリのファイルを見比べて、古い方を新しい方に寄せていくというやり方でやってみましょう。後で削除する予定のsetContentViewがアクティビティのonCreateメソッドの中にありますが、その位置にsetContentを追加してみます。これが新しいレイアウトシステムComposeってやつの第一歩ですね。・・・setContentはそれだけでは動かないようです。

アクティビティの中でsetContentを呼ぶにはComponentActivityを継承している必要があるようです。これまではAppCompatActivityというのを継承していたみたいなので、こちらと交換しました。どうもAppCompatActivityはComponentActivityのサブクラスという扱いになっているようです。あれ、これでもsetContentが呼べません。

import androidx.activity.compose.setContent

あぁ、直接importするものだったのか、とimport文を追加してみましたが、compose部分が赤字になってしまいました。うん、これはモジュールの依存関係絡みですかね。build.gradleファイルにcomposeと書いてあるやつを全部コピーしてみました。

    implementation("androidx.activity:activity-compose:1.9.3")
    implementation(platform("androidx.compose:compose-bom:2024.11.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    androidTestImplementation(platform("androidx.compose:compose-bom:2024.11.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

どれが何を指しているかちゃんとは調べてませんが、とりあえずsetContentは呼べるようになりました。

テーマ、色、フォント

さて参考にしている新しいレイアウトシステムの方のアプリでは、setContentの直下にNewLayoutAppThemeという要素が配置されていました。(「NewLayoutApp」はアプリ名とお考えください。)これはどうもAndroid Studioが自動的に生成したレイアウトのテーマを司る要素のようです。場所はKotlinのソースファイルが置かれる場所に「ui/theme」というサブディレクトリを掘って、その中に以下の3つのファイルを発見しました。

  • Color.kt - 色を定義している
  • Theme.kt -カラースキーマを定義して、ComposableなNewLayoutAppTheme要素を定義している
  • Type.kt - フォントを定義している

古いレイアウトシステムの時はcolors.xmlっていうリソースファイルに色の定義を書いていましたが、Color.ktはその代わりになりそうですね。とりあえず、この3つをコピーして、package情報を変更中のアプリに合わせます。NewLayoutAppThemeっていうのもアプリに合わせて変更しておきます。あっ、テーマファイルの中でwindow.statusBarColorっていうのをいじっている個所が、deprecatedされて取り消し線が引かれてる・・・。一旦、無視しましょう。これもAndroid15の影響らしい)とにかくこういうComposeな要素の基本的な設定をまとめたものがテーマと呼ばれて、各Compose要素の上位に配置されるものなのだな、という雑な理解を得ました。

gradleとの闘い

そろそろ一回、ビルドしてみたくなりませんか?私はなります。思いつくままに実装をしていると、書いているときは楽しいのですが、大量に書き散らしたものをビルドが一発で通ったことはあんまりなく、かなり前に書いた部分の問題が原因だと、そこまで振り返って修正するのに、だいぶ記憶をさかのぼる必要が生じてしまうのでした。だから、たまにビルドしてみて、どこかに問題がないかコンパイラさんたちに聞いておくのです幸い最近の開発環境さんは事前に問題になる部分を勝手にまとめて「Problems」パネルにまとめてレポートしてくれるので、それが空なら問題なくビルドは通るはず・・・通らない。二つほど何かあったことがBuildパネルに書かれています。読みにくい英文で、多分gradleが期待した動きをしていないみたいです。ここであーでもない、こーでもないといろいろ試し始めて、激しく時間をロスしています。本当にgradleのことでいい経験がなくて、何の苦行をさせられているんだろうかって思っていますよ。ログをよく読めって、まぁよく言われるし、実際そうなんですが、ログの表現が自分と相性が悪いんですかね?長くても読めるログと、読む気が起きないログの差ってどこででるんでしょう?最近はAIさんにログを丸投げするっていう手法も選択肢にあってとても助かります。でもググってた時代が長くて、怪しげなログの一節を検索にかけてみたり。

implementation("androidx.compose.material3:material3")

結局、モジュールの依存関係を削ってビルドを試して、ビルド結果を比べてみる方法(地道)によってあぶりだされたandroidx.compose.material3モジュールのことを調べて、Composeを使うには、build.gradleファイルに別途設定が必要だと知りました。

android {
    buildFeatures {
        compose = true
    }
    composeOptions {
        kotlinCompilerExtensionVersion = "1.5.15"
    }
    kotlinOptions {
        jvmTarget = "19"
    }
}

ん?しかし、まだビルドが通らず。Kotlinのコンパイラのバージョンが2.0.0に上がってCompose Compiler Gradle pluginを使って・・・(何を言っているのかわからない。)

ビルドが通らないまま1月ほど経ちました。(まだ通ってない)一月あるとテスト端末のOSにもバージョンアップが入ったり、開発環境のモジュールもバージョンアップしたり、何かが好転してくれたらいいのですが、とりあえずダメでした。kotlinCompilerExtensionVersionを変更したら、ビルドエラーログが変わりました。ここに不整合があって、ビルドが通らなかったってことだと思われます。すると次に、setContentが参照できないと言われました。

import androidx.activity.compose.setContent

↑ここからimportして、

dependencies {
  implementation 'androidx.activity:activity-compose:1.9.3'
}

依存関係に↑これを追加しろってことらしい。

org.jetbrains.kotlin.backend.common.BackendException: Backend Internal error: Exception during IR lowering

しかし、まだ謎の例外が飛んできます。それと一緒にずっとAGPがSDKのバージョンと合ってないよって言われてました。AGP(Android Gradle Plugin?)って選択できる最新バージョンを選んでるのに、調べるとまだうえがあるらしい・・・なぜ選べないのか?ここで、AndroidStdioをコアラ(K)からてんとう虫(L)に変更してみることに。あっ、結構UIが刷新されてるぞ。どこに何があるかわからぬ・・・。でもUpgrade assistantがAGPを8.7.3まで上げてくれるみたいだ!でも直接的には関係なく、コンパイルエラーは止まりません。あっ、ladybug版はタブが文字からアイコンに切り替わりましたが、慣れるとこっちの方が見やすいかも。

dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
    val context = LocalContext.current
    if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}

結局謎の例外は Theme.kt の↑この部分をコメントアウトしたら収まりました。別のプロジェクトでは、あってもコンパイル通るのにな。SDKのバージョンと関係した何かがどこかでひっかかるんでしょうね。深く考えたくないので、一旦コメントアウトしたまま進みます。

setContent

ビルドが通ったのはいいのですが、次は実行時にエラーが出て、アプリが止まっているようです。setContentが NoSuchMethodError を吐いていると・・・。逆にそれでビルドって通るものなんですかね!?

ここでGeminiを使ってみようと思い立ち、Android Studioの右ペインから呼び出してみました。Geminiは初めての利用です。ChatGPTとか、あと最近はXのGrokなんかも利用制限が解け、生成AIになんでも聞いちゃうわけですが、Androidアプリの開発については、Geminiさんとかいい仕事してくれそうな予感です。それで提示された回答は

  • ビルド時とランタイム時でメソッド名が変わることありまっせ
  • 何かしらのライブラリやモジュールのバージョンが整合性を保ってないとこういうことあるよね
  • あとProGuardとかR8がメソッド名書き換えちゃうこともあるね

↑こんな感じでした。AIがもう勝手に直してくれればいいのに、ってちょっと思いましたが、具体的な対策をいくつか提示してくれたので、それに従ってみるのでした。結果、変わらず、というか示された対策がちゃんとできているか、ちょっと自信が持てなかったです。

年越しを経て、何か次の手を打とうと考えて、前々から考えていた「build.gradleファイルをKotlin形式に書き換える」を実行してみました。旧来のタイプはGroovy形式ていうんですかね?最近ググって出てくるのがこっちじゃなく、Kotlin形式なことも増えたし、Android Studioで新規プロジェクトを立ち上げるとKotlin形式のファイルができるし、世界はKotlin化しているのかもしれません。プログラムもKotlinで書くし、設定ファイルも同じように書けた方が学習量が減っていい気もします。具体的にはbuild.gradleファイルをbuild.gradle.ktsにリネーム(リファクタリング)して、書式に文法エラーが出てるところを地道に書き換えるだけでした。結果、それだけでは何も変わりませんでしたが、「比較用に新規に立ち上げたプロジェクト雛形」と比較がしやすくなりました。新しいプロジェクト雛形の方はgradle/libs.versions.tomlていうファイルにモジュールのバージョン情報とかまとめる書き方に変わってたんですよ。それも全部真似してみました。しかし、実行時のNoSuchMethodErrorは出続けるのでした。でも、何か見つかりましたよ。

kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" }
kotlin-gradle-plugin-v1710 = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlinGradlePluginVersion" }

libs.versions.tomlにライブラリのバージョン情報を転記してもらった結果、バージョン違いの同じライブラリをどこかで呼んでいたことが発覚しました。

kotlinGradlePlugin = "1.9.10"
kotlinGradlePluginVersion = "1.7.10"

バージョン番号はマイナーバージョン二つ分違ってるようで、これを新しい方に合わせてみると・・・ビルドエラーが出ました。これはすぐ直せましたが、NoSuchMethodErrorはまだ出ています。 ん?日付をまたいで再度Android Studioでビルドしたら、NoSuchMethodErrorが消えてました。キャッシュが残ってただけだったんでしょうか。代わりにfindViewByIdのところでNullPointerExceptionが起こるようになりました。NoSuchMethodErrorは結局、バージョン重複部分を解消したら直った、ということにしておきましょう。次はsetContentViewでレイアウトXMLファイルを読み込んでいた部分を、setContentで適当なレイアウトをにしたせいで、画面のパーツ(View)をfindViewByIdでみつけられなくて操作できなくなったところを修正していきます。

Composableに移行する2

一覧へ