Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Eight Android アプリで Kotlin 2.0 を試してみた

技術本部 Mobile Applicationグループ所属の大塚です。

名刺アプリ「Eight」のAndroidアプリの開発と、営業DXサービス「Sansan」とEightの両プロダクトをまたぐプロダクト横断チームの一員として、モバイル領域の中長期的な技術的課題の解決や、PoCの開発を担当しています。

今回は正式リリースされたKotlin2.0をEight Androidアプリのプロジェクトで試してみましたので、その知見を共有します。

公式が公開しているKotlin 2.0の変更点はこちらです。正確な情報はリンク先を参照してください。
kotlinlang.org

Kotlin 2.0の主要な変更点

Kotlin 2.0の大きな変更点としてK2コンパイラが安定版となり、デフォルトのコンパイラとして使われるというものです。
(※ 説明の便宜上、これ以降は1.9でデフォルトだったコンパイラをK1コンパイラと表記しています。)

K1コンパイラのコンパイラフロントエンドはPSIと呼ばれる構文木とBindingContextと呼ばれるセマンティック情報を持つテーブルを出力していました。K2コンパイラからはフロントエンドIRとして、セマンティック情報が直接格納された構文ツリーを出力するという変更が行われました。

K2コンパイラを使用することで、次のような恩恵を受けられます。

  • 型推論の改善
  • IDEのパフォーマンス向上
  • コンパイル時間の短縮

環境構築

Kotlin 1.9.xからgradle.propertiesファイルに次のように記載することで、K2コンパイラを試すことができました。

kotlin.experimental.tryK2=true

Kotlin 2.0からはデフォルトでK2コンパイラが使用されるので、こちらの指定は不要になります。

また、Kotlinのバージョンを2.0にした場合はJetpack ComposeのComposeコンパイラバージョンを指定するのではなく、Compose Compiler Gradle Pluginを利用する必要があります。
Compose Compiler Gradle Pluginにより、KotlinバージョンとCompose Compiler Versionの依存が不要になるというメリットがあります。Compose Compiler Gradle Pluginの追加は次の通りで、他のGradle Pluginと変わりません。

plugins {
    id "org.jetbrains.kotlin.plugin.compose" version "$kotlin_version" 
}

プラグインを追加した後はコンパイラオプション用のDSLを利用可能です。オプションの一覧は次の通りです。
Compose compiler | Kotlin Multiplatform Development Documentation

環境構築は以上です。

Eight Androidアプリのプロジェクトで試験的にKotlin 2.0に引き上げて試してみたところ、いくつかビルドエラーは起こりましたが大きな修正なくアップデートできました。

型推論の改善

K2コンパイラを利用することでスマートキャストがさらに改善します。公式が紹介していたものは6つのパターンでした。

  • ローカル変数とさらなるスコープ
  • 論理または演算子による型チェック
  • インライン関数
  • 関数型を持つクラスのプロパティ
  • 例外処理
  • インクリメントとデクリメント演算子

「インライン関数」、「関数型を持つクラスのプロパティ」、「例外処理」、「インクリメントとデクリメント演算子」は、現状のEight Androidアプリ開発においてあまり使うことはなさそうでしたので説明は割愛します。公式で紹介されている事例は下記のリンクから確認できるので、気になる方はご確認ください。
What's new in Kotlin 2.0.0 | Kotlin Documentation

「ローカル変数とさらなるスコープ」、「論理または演算子による型チェック」は活用できるシーンがありそうなので掘り下げてみます。

ローカル変数とさらなるスコープ

今まではif文の条件内で変数がNULLでないと評価された時にスマートキャストが適用されていました。Kotlin2.0からはif文の前に変数を宣言すると、変数について収集した情報を使ってスマートキャストが適用されます。

真偽値を変数に取り出したい場合において、if文の外で意味のある名前で変数を宣言することで可読性が向上します。アプリ内のドメインクラスやUIの状態を判断する処理において同様の使い方をしている部分があるので、必要な場合は変数に取り出して書けそうです。

sealed class Sample() {
    class Sample1(val sample1: String) : Sample()
    class Sample2(val sample2: String) : Sample()
}

// K1での動作
fun k1Function(sample: Sample) {
    if(sample is Sample.Sample1) {
        sample.sample1
    }
}

// K2で可能になったこと
fun k2Function(sample: Sample) {
    val isSample1 = sample is Sample.Sample1
    if(isSample1) {
        // K1では Sample1 にスマートキャストが適用されなかった
        sample.sample1
    }
}
論理または演算子による型チェック

K1コンパイラではOR条件の場合if文の中で型のチェックを行う必要がありましたが、Kotlin2.0からは条件に一致した最も近い共通のスーパータイプにスマートキャストが適用されるようになります。

Eight Androidアプリ内でも一部の複雑なドメインオブジェクトで継承階層が深い場合があります。条件分岐を多用してUI Stateなどのデータを生成するケースで活用できると思いました。

interface Top { fun doTop() }
interface Middle1: Top { fun doMiddle1() }
interface Middle2: Top { fun doMiddle2() }
interface Bottom1: Middle1 { fun doBottom1() }
interface Bottom2: Middle1 { fun doBottom2() }
interface Bottom3: Middle2 { fun doBottom3() }

fun signalCheck(tmp: Top) {
    if (tmp is Bottom1 || tmp is Bottom2) {
        // Middle1にスマートキャストが適用される
        // K1だとTopになってしまう
        tmp.doMiddle1()
        tmp.doTop()
    }
    if (tmp is Bottom1 || tmp is Bottom3) {
        // Topにスマートキャストされる
        // K1でも同じ
        tmp.doTop()
    }
}

IDEのサポート

Kotlinのバージョンを2.0にアップデートしてもIDEはK2コンパイラを使用していないので、K2から利用可能になったスマートキャストなどがエラーとしてマークされてしまいます。

Android Studio KoalaからK2 Kotlin Modeとして、K2コンパイラを分析エンジンとして使用するモードがアルファ版として登場していますが、こちらはAndroidプロジェクトに対応していないため、コード補完などが機能しませんでした。Kotlin Libraryとしてモジュールを追加した場合は機能しましたが、Android Libraryとして追加した場合にK1コンパイラを使うようにフォールバックするなどの機能もないため、Androidアプリ開発においてはまだ十分な機能とはいえませんでした。

ビルド時間の短縮

Eight Androidアプリのプロジェクトで、Kotlinのバージョンを1.9.22から2.0に変更し、ビルド時間を計測しました。

ビルド時間は、次のコマンドを使用して--profileオプションで測定しました。

./gradlew clean && ./gradlew --profile assemble{$Flavor}Debug --no-daemon --no-build-cache --no-configuration-cache --max-workers=1 --offline --rerun-tasks

全体のビルド時間は、Kotlin 1.9.22で369秒、Kotlin 2.0で353秒、つまり16秒(約4%)短縮されました。

補足になりますが、EightAndroidアプリではKaptからKSPへの移行が完了していません。Kaptのスタブ生成が占めている時間は大きいため、ビルド速度の改善という観点ではこちらも重要な課題となりますが、今回はコンパイルタスクに絞って調査したので割愛します。

コンパイル時間の比較

Kotlinのコンパイルタスクに絞って見ると、Kotlin 1.9.22で101秒、Kotlin 2.0で80秒と、21秒(約20%)短縮されました。モジュールごとのコンパイルタスクの実行時間を比較したところ、多くのモジュールでコンパイル時間が短縮されました。モジュールとコンパイルタスクの実行時間の関係は図1の通りです。最も短縮されたモジュールでは、1.52秒(22.64%)の短縮が見られました。

一部のモジュールではコンパイル時間がわずかに増加する場合もありましたが、複数回実行するとビルド時間が若干減少することも観察されました。これらのモジュールは、大きな変化はなかったと考えられます。

図1. モジュールごとのコンパイル時間

コンパイル時間が減少したモジュールとそうでないモジュールには何かしらの違いがあると考えました。Eight Androidアプリのプロジェクトには、機能ごとのUIやActivity, ViewModelなどのクラスが含まれる「Componentモジュール」と、SDKやライブラリに依存しないKotlinクラスが多く存在する「Domainモジュール」があります。図2のグラフの通り、コンパイル時間が大きく減少したのはComponentモジュールで、Domainモジュールでは大きな変化がありませんでした。


図2. コンパイル時間が減少したモジュールとそうではないモジュール

コンパイルタスク内での実行時間の変化

コンパイルタスクの詳細を確認するため、次のフラグをgradle.propertiesに設定し、詳細なレポートを出力しました。

kotlin.build.report.output=file

ビルドレポートの読み方やコンパイルの各フェーズについては、JetBrainsの公式ドキュメントをご覧ください。
Introducing Kotlin Build Reports | The Kotlin Blog

コンパイル時間が減少しなかったDomainモジュールと、最も短縮されたComponentモジュールのコンパイル時間を比較してみました。Componentモジュールのコンパイル時間は、K1コンパイラで4.09秒、K2コンパイラで2.65秒でした。各コンパイラのフェーズごとの内訳は表1、表2の通りです。

K1コンパイラとK2コンパイラではフロントエンドIRが追加されたことでフェーズが異なります。各フェーズの説明はJetBrainsのブログの中で紹介されています。


表1. K1コンパイラを使用した場合のComponentモジュールにおけるコンパイルタスクの内訳
フェーズ 実行時間(秒)
Compiler initialization time
0.11
Compiler code analysis
2.17
Compiler code generation
1.09

表2. K2コンパイラを使用した場合のComponentモジュールにおけるコンパイルタスクの内訳
フェーズ 実行時間(秒)
Compiler initialization time
0.05
Compiler code analysis
1.02
Compiler IR translation
0.65
Compiler code generation
0.92


次に、Domainモジュールを確認しました。Domainモジュールのコンパイル時間は、K1コンパイラで7.03秒、K2コンパイラで7.27秒でした。各コンパイラのフェーズごとの内訳は表3、表4の通りです。


表3. K1コンパイラを使用した場合のDomainモジュールにおけるコンパイルタスクの内訳
フェーズ 実行時間(秒)
Compiler initialization time
0.13
Compiler code analysis
3.14
Compiler code generation
2.73

表4. K2コンパイラを使用した場合のDomainモジュールにおけるコンパイルタスクの内訳
フェーズ 実行時間(秒)
Compiler initialization time
0.06
Compiler code analysis
3.14
Compiler IR translation
1.28
Compiler code generation
2.77


Componentモジュールの内訳を確認すると、すべてのフェーズでK2コンパイラの方が実行時間が短いことが確認できます。特に分析フェーズの実行時間の減少が著しく効果があると言えます。Domainモジュールでは「Compiler initialization time」の実行時間は減少していますが、それ以外で実行時間の減少は確認できませんでした。

コンパイル時間が減少したモジュールの考察

高速化の恩恵が受けられなかった原因を探るために、2つのモジュールの違いを調査しました。表5の通りですが、クラス数とモジュールに含まれるクラスが大きな違いだと思います。

Componentモジュールのクラスには、多くの処理が含まれており、他のクラスへの参照も多数発生します。K2コンパイラでは、1つのデータ構造を管理するだけで済むため、こうした処理が多いクラスが多数を占めるモジュールで、分析フェーズの改善が見られた可能性があります。

一方、Domainモジュールは基本的にデータ構造を定義しているだけなので、大きな変化がなかったのではないかと考えています。

この推測はあくまでも仮説であり、今後のビルド速度改善に向けて、さらなる調査が必要です。


表5. 2つのモジュールの比較
     Componentモジュール Domainモジュール
クラス数
198
676
モジュールに含まれるクラスの役割
Activity、ViewModel、RecyclerCViewAdapterなどUI関連のコード、DaggerのModuleとComponent
data classやsealed classなどでデータ構造を表現したシンプルなクラス、RepositoryなどのInraface


まとめ

Kotlin 2.0にアップデートすることで型推論の改善やコンパイル時間の短縮が実現されました。K2コンパイラを利用することで、スマートキャストがさらに改善され、コードの可読性が向上し、複雑な条件分岐への対応がしやすくなります。ただし、現時点でIDEのサポートは完全ではなく、特にAndroidプロジェクトでの利用には課題があります。ビルド時間の短縮については、Kotlinのバージョンを1.9.22から2.0に変更することで、ビルド時間を短縮できました。ただし、効果が見られたモジュールとそうでないモジュールがあり、ビルド速度の改善のためさらなる調査をしていきます!

© Sansan, Inc.