Kotlin by JetBrains「Jake Wharton | KotlinConfersations'26」の文字起こし

Android開発やKotlinコミュニティのキーパーソンであるジェイク・ウォートン(Jake Wharton)氏へのインタビューです。

 

🤔 1. 自己紹介とKotlinとの歩み [00:26]

経歴:
長年Androidデベロッパーとして活動。Square(現Block)、Cash Appを経て、現在はSkylightに在籍。

Kotlinとの出会い:
Square時代、Java 7の機能不足やGoogleのツール進化の遅さに悩んでいた「プレ1.0(正式リリース前)」の頃にKotlinに注目。社内向けに導入提案書(プロポーザル)を作成。

反響:
その提案書を公開したところコミュニティで大きな話題となり、SquareでのKotlin導入だけでなく、エコシステム全体の盛り上がりに繋がった。

 

🤔 2. AndroidにおけるKotlinとコミュニティの力 [01:50]

Googleでの活動:
一時期Googleに籍を置き、AndroidにおけるKotlinの公式サポート(KTXライブラリなどの立ち上げ)を支援。

エコシステムの進化:
KTXライブラリのコード自体は現在通常のライブラリに統合されて役目を終えたが、それは言語が成熟した良い証拠であると語る。

Apple(Swift)とのアプローチの違い:
iOSのSwiftがAppleという中央集権からトップダウンで提供されたのに対し、AndroidにおけるKotlinは「草の根(グラスルーツ)的なコミュニティの熱意」から始まり、最終的にGoogleが公式サポートせざるを得ない流れを作った点がユニークである。

非Androidへの広がり:
「KotlinはGoogle製でも、Android専用でもない」という点が、15年経った今ではAndroid以外の開発者にも広く認知されるようになった。

 

🤔 3. Kotlin Multiplatform (KMP) の挑戦 [05:35]

これまでのクロスプラットフォーム:
過去のXamarinやPhoneGap、現代のReact NativeやFlutterなどを評価した上で、Cash App時代にKotlin Multiplatform(KMP)とCompose Multiplatformを選択。

アプローチの特徴:
UI層はiOSならUIKit/SwiftUI、WebならHTML DOM、AndroidならCompose UIといった「各プラットフォーム独自のネイティブビュー」を尊重し、バックエンドのビジネスロジックやプレゼンターロジックのみを共通化(シェア)する方針をとった。

KMPの強み:
他のクロスプラットフォーム言語と異なり、WebならJavaScript、iOSならネイティブコード、AndroidならJavaバイトコードと、ターゲットごとに最適な形へコンパイルされるため、メモリ空間の競合や相互運用の壁(Interop layer)が少ない。

エコシステムへの貢献:
5年前に始めた当時はJetBrainsのComposeも初期段階だったため、自分たちで足りないターゲットを作るなどしてエコシステムを強力にプッシュした。

 

🤔 4. オープンソース(OSS)の重要性 [10:53]

キャリアへの影響:
オープンソースに貢献することで、自身のスキルアップだけでなく、新しい人との出会いや転職の機会など、キャリアの節目で何度も救われた。

企業とOSSの関係:
REST APIとの通信や画像読み込み、依存関係の解決(DI)など、どのアプリでも共通する「ビジネスの知的財産(IP)ではない部分」のコードはオープンにすべき。結果的に会社の採用活動(「OSSを見て応募した」という優秀な人材の獲得)など、数字に表れにくい大きなリターン(無形の資産)をもたらす。

 

🤔 5. 生成AI(LLM)とライブラリの未来 [15:11]

「AIがコード生成できるならライブラリ投資は不要か?」という問いへの持論:
「コードを書くこと(Writing code)」自体は、ソフトウェア開発において決して最も難しい部分ではない。AIは既存のスキルを加速させる(アクセラレーター)ツールとして使うべきであり、それなしではコードが書けないような依存の仕方はリスクがある(将来的なコスト高騰も含めて)。

コードは書かれる回数より、読まれる回数の方が10倍多い(Code is read 10 times more than it's written)。 要件は常に変わるため、全体を俯瞰して理解できるコード設計が重要。

ライブラリの役割:
ライブラリの真の価値は「再利用可能なコードの境界線(デリミテーション)」を明確に引き、人間の認知負荷を下げることにある。パレートの法則のように「80〜85%の共通ユースケース」を綺麗にカプセル化することが大切。人間にとって直感的で優れた設計は、言語モデル(AI)にとっても扱いやすいはずである。

 

🤔 6. 2026年現在のKotlinへの期待 [22:45]

K2コンパイラの恩恵:
長年開発が続けられてきた「K2コンパイラ」への移行(大きな山場)を乗り越えたことで、言語自体の進化スピードが再び加速している。

注目している新機能:
Rich Errors(エラー処理の改善)プロポーザルの刷新
未使用の戻り値チェッカー(Unused return value checker)
when 式のデフォルト網羅性(Exhaustive when by default)

現在の心境:
K2の開発に全力を注いでいた停滞期を抜け出し、標準ライブラリ(datetimeやco-routinesなど)や言語プロポーザルが活発に進化している今の状況は、初期のKotlinのワクワク感を思い出させる。

 

🤔 7. コミュニティへの参加に気後れしている人へのアドバイス [27:30]

提案書(KEEP)の難しさ:
KEEP(Kotlin Evolution and Enhancement Process)はコンパイラの構文解析や各プラットフォームのバイトコードまで考慮しなければならず、内容が非常に高度で圧倒されるのは当然。

おすすめの関わり方:
Kotlin公式Slack(Kotlinlang Slack)にある language-proposal や language-evolution チャンネルを覗いてみるのがおすすめ。そこでは、より人間的で身近な困りごとや質問が、消化しやすい形で活発に議論されている。


Androidアーキテクチャの現状:GoogleのUDFと現実世界の「MVI風MVVM」の比較

現代のAndroid開発においてどのアーキテクチャを採用するかを議論する際、議論はほぼ常にMVVM対MVIのどちらにするかという点に集約されます。

しかし、近年の傾向を見ると、この2つは対立する力ではなく、収束しつつあることは明らかです。

 

🧑🏻‍💻 Googleの立場:特定のパターン名よりもUDFを優先

Googleの公式「アプリアーキテクチャガイド」を詳しく見てみると、「MVI」という用語はほとんど出てこないことに気づくでしょう。その代わりに、Googleは一貫してUDF(単方向データフロー)という概念を推奨しています。

Googleは、コミュニティに既に馴染みのある「MVI」ではなく、なぜ抽象的な用語である「UDF」を使用するのでしょうか?

その理由は、Googleが柔軟性を重視していることにあり、彼らの哲学は次のようなものと思われます。

「MVIのような厳格な命名規則にとらわれすぎないでください。アーキテクチャの本質である一方向のデータフローが維持されるようにするだけで十分です。」

Reduxのような厳密な状態管理ライブラリを使用する場合でも、シンプルなViewModelで実装する場合でも、Googleが重視するのは「状態は下へ流れ、イベントは上へ流れる」という基本原則に従うことです。

 

🧑🏻‍💻 現代の標準:MVI風MVVMの実装

現場では、Googleが推奨するUDFに対する最も一般的なアプローチは、私が「MVI風味のMVVM」と呼ぶもので、MVIルールをMVVMコンテナに詰め込むものです。これには、状態、意図、効果という3つのコア要素が含まれます。

実際には以下のようになります。


Hiltのナビゲーション引数で SavedStateHandle から卒業する

なぜ Hilt 2.49+ と @AssistedInject が、完全な型安全性を備えた引数渡しの「正解」と言えるのか。

 

🤔 問題点:静的な依存関係と動的なデータの混在

Android開発において、ViewModelにランタイム引数(実行時引数)を注入することは、常に議論の的となってきました。

その本質は、「静的依存関係」(DIによって管理されるリポジトリなど)と、「動的データ」(画面遷移時に渡されるナビゲーション引数など)の対立にあります。

適切なDIパターンを適用せずにこの問題に対処しようとすると、高確率でアンチパターンに陥ります。たとえば、一時的な状態をシングルトン(Singleton)内に保持してしまうと、状態の汚染や画面間でのデータ漏洩といった致命的なバグを引き起こす危険性があります。

 

🤔 解決策:ハイブリッド注入を実現する @AssistedInject

この「静的」と「動的」のギャップを埋めるための最適なツールが、@AssistedInject です。これを使用することで、DI(Hilt)が管理するオブジェクトと、実行時に受け取るランタイムパラメータをスマートに組み合わせた「ハイブリッド注入」が可能になります。

通常の @Inject は不要 :
@AssistedInject を使用する場合、コンストラクタに通常の @Inject を付ける必要はありません(というか、付けてはいけません)。

関心の分離 :
リポジトリなどの依存関係は Hilt が自動で供給し、ナビゲーション引数などの動的なデータだけを手動で安全に渡す、という明確な役割分担(関心の分離)が実現します。

クリーンなコードへ :
これにより、バグの温床になりがちだった危険な lateinit var による後からの初期化コードとは、完全におさらばできます。


// ViewModel 実装

@HiltViewModel(assistedFactory = RouteBViewModel.Factory::class) 
class  RouteBViewModel  @AssistedInject constructor( 
    private val repository: MyRepository,
    @Assisted val navKey: RouteB,
) : ViewModel() { 

    @AssistedFactory
    interface Factory { 
        fun create (navKey: RouteB) : RouteBViewModel 
    } 
}

 

🤔 実装:assistedFactory と hiltViewModel の組み合わせ

公式の nav3-recipes リポジトリでは、Hilt 2.49 以降で導入された最新の標準的な実装パターンが示されています。

ファクトリの宣言 :
@HiltViewModel(assistedFactory = ...) を使用して、Assisted Factory を ViewModel に直接リンクさせます。

2つの型パラメータ: Compose 側では、hiltViewModel() のように両方の型を明示的に指定して呼び出します。

安全なライフサイクル管理 :
creationCallback を利用することで、ナビゲーションのバックスタック管理(Lifecycle)を壊すことなく、安全にランタイム引数を渡すことができます。


// UI(ナビゲーション定義)

 entry<RouteB> { key -> 
    val viewModel = hiltViewModel<RouteBViewModel, RouteBViewModel.Factory>( 
        creationCallback = { factory -> 
            factory.create(key) 
        } 
    ) 
    ScreenB(viewModel = viewModel) 
}

 

🤔 メリット:100%の型安全性を実現し、SavedStateHandle のボイラープレートを完全に排除

このアーキテクチャ(Navigation 3 + @AssistedInject)に移行することで、コードベースの品質は大幅に向上します。

完全な型安全性(100% Type-Safe):
かつての文字列ベースのキー(string のキー指定)は過去のものです。ナビゲーション引数は、Route(Serializable等で定義された型)として、厳密に型指定されたオブジェクトのまま直接安全に渡されます。

SavedStateHandle からの解放 :
単に次の画面へ引数を渡すだけのために、SavedStateHandle を使ってごにょごにょと値を書き出したり読み出したりする定型文(ボイラープレート)はもう一切不要になります。

圧倒的なリファクタリングのしやすさ :
すべてがコンパイル時にチェックされるため、引数の追加・削除・変更といったリファクタリングが、一瞬かつ確実に(ランタイムエラーの心配なく)行えるようになります。

 

🤔 まとめ

Hilt と最新の Jetpack Navigation の相乗効果により、画面間の引数の受け渡しは驚くほどスムーズかつ洗練されたものになりました。もはや、型安全性を犠牲にしたり、不自然なアーキテクチャで回避策(ワークアラウンド)を講じたりする必要はありません。

@AssistedInject と、進化した hiltViewModel() API を組み合わせることで、「完璧な型安全性」と「クリーンなハイブリッド依存性注入(DI)」を今すぐあなたのプロジェクトに導入できます。


SavedInstanceState 不要!? Navigation3 の NavKey がもたらす Jetpack Compose 開発の変革

Android 開発者を長年悩ませてきた「画面回転」や「プロセス死」に伴う状態保存。SavedInstanceStateSavedStateHandle と格闘する日々は、もう過去のものになろうとしています。

Jetpack Navigation3 のソースコードを読み解くと、新しく登場した NavKey という仕組みが、Compose 時代の状態保存をいかにスマートに変革しようとしているかが見えてきました。

 

🤔 1. NavKey:単なる「マーカー」ではない、型安全の要


/**
 * Marker interface for keys.
 *
 * Objects and classes that extend this class must be marked with the [Serializable] annotation in
 * order to be saved with by the [rememberNavBackStack] function.
 *
 * This class is required because [Serializable] is only an annotation and does not provide a way to
 * link classes marked with the annotation together and provide a serializable that works with all
 * of them, resulting it making it impossible to properly save and restore.
 */
public interface NavKey

Navigation3 でバックスタックを管理する rememberNavBackStack を使う際、避けて通れないのが NavKey インターフェースの実装です。一見すると中身のないマーカーインターフェースですが、これこそが 「型安全な自動保存」 の鍵を握っています。

従来の Navigation では、引数の受け渡しや状態保存の裏側で Bundle が使われてきました。しかし、Bundle は何でも入る反面、型安全性が欠け、実行時のエラーを招きやすいという課題がありました。


/**
 * Provides a [NavBackStack] that is automatically remembered in the Compose hierarchy across
 * process death and configuration changes.
 *
 * This overload **does not take a [SavedStateConfiguration]**. It relies on the platform default:
 * on **Android**, state is saved/restored using a **reflection-based serializer**; on **other
 * platforms this will fail at runtime**. If you target non-Android platforms, use the overload that
 * accepts a [SavedStateConfiguration] and register your serializers explicitly.
 *
 * ### When to use this overload
 * - You are on **Android only** and want a simple API that uses reflection under the hood.
 * - Your back stack elements use **closed polymorphism** (sealed hierarchies) or otherwise work
 *   with Android’s reflective serializer.
 *
 * ### Serialization requirements
 * - Each element placed in the [NavBackStack] must be `@Serializable`.
 * - For **closed polymorphism** (sealed hierarchies), the compiler knows all subtypes and generates
 *   serializers; Android’s reflection will also work.
 * - For **open polymorphism** (interfaces or non-sealed hierarchies):
 *     - On Android, the reflection path can handle subtypes without manual registration.
 *     - On non-Android, this overload is **unsupported**; use the configuration overload and
 *       register all subtypes of [NavKey] in a [SerializersModule].
 *
 * @sample androidx.navigation3.runtime.samples.rememberNavBackStack_withReflection
 * @param elements The initial keys of this back stack.
 * @return A [NavBackStack] that survives process death and configuration changes on Android.
 * @see NavBackStackSerializer
 */
@Composable
public fun rememberNavBackStack(vararg elements: NavKey): NavBackStack<NavKey> {
    return rememberSerializable(
        serializer = NavBackStackSerializer(elementSerializer = NavKeySerializer())
    ) {
        NavBackStack(*elements)
    }
}

NavKey は、バックスタックに入るすべての要素が「シリアライズ可能であること」をコンパイル時に保証するための型上の制約として機能します。

 

🤔 2. なぜ「Serializable アノテーション」だけでは不十分か

Kotlin Serialization を使っているなら、@Serializable だけで十分だと思うかもしれません。しかし、ソースコードのコメントには重要な洞察が記されています。

Serializable はあくまでアノテーションであり、クラス同士をコード上でリンクさせる手段を提供しない。そのため、すべての要素を適切に保存・復元することが不可能になる。

NavKey という共通のインターフェースを介することで、Navigation 側は「このバックスタックの中身はすべて、共通の手順でシリアライズできる仲間である」と認識できるようになります。これが、「何も考えなくても状態が復元される」 という体験の裏側にあるロジックです。

 

🤔 3. Android 限定の「リフレクション」がもたらす魔法

Navigation3 の rememberNavBackStack には、Android 開発者にとって非常に強力な恩恵があります。

Android プラットフォーム上では、リフレクションベースのシライライザーがデフォルトで動作します。これにより、開発者はシリアライザーを個別に登録する手間(ボイラープレート)から解放されます。

  • 開発者: NavKey を継承し、@Serializable を付ける。
  • Navigation3: process death の際、リフレクションを使ってバックスタックをまるごと保存し、復帰時に自動で復元する。

これこそが、「さらば SavedInstanceState」と言える最大の理由です。

 

🤔 4. Navigation3 時代の実装スタイル

これからの Compose 開発では、以下のように NavKey を実装した sealed interface を定義するのが標準になるでしょう。


@Serializable
sealed interface Screen : NavKey {
    @Serializable
    data object Home : Screen

    @Serializable
    data class UserProfile(val userId: String) : Screen
}

// Composable 内での利用
val backStack = rememberNavBackStack(Screen.Home)

この実装だけで、userId を含めた遷移状態が、OSによるメモリ回収の後でも完璧に復元されます。もはや Bundle を意識するシーンはほとんどなくなるはずです。

 

🤔 まとめ

Navigation3 における NavKey の導入は、Google が掲げる開発思想の現れのように感じます。

内部の複雑なシリアライズ処理を NavKey という一つのインターフェースに集約し、開発者には最小限の実装(アノテーションと継承)だけで最大限の恩恵(自動状態保存)を与える。

「状態保存に怯える開発」はもう終わりです。Navigation3 を武器に、より本質的な UI 実装に集中していきましょう。


Embracing AGP 9 and JDK 21: A Gradual Path to Android Build Optimization

As Android developers, major AGP (Android Gradle Plugin) updates are always significant. AGP 9 in particular promises a stricter, faster build environment, moving away from ambiguous configurations.

Instead of waiting for its full release and then scrambling to fix issues, why not start preparing your project now, gradually aligning it with "AGP 9 standards" through your gradle.properties file?

 

🤔 Why JDK 21 and AGP 9 Now? (The Ultimate Synergy)

When transitioning to AGP 9, updating to JDK 21 isn't just a "requirement"; it's a powerful "booster" that dramatically enhances your development experience.

  • Performance Synchronization: JDK 21's improved resource management, including features like Virtual Threads, allows Gradle to fully leverage its parallel build capabilities, leading to more stable and efficient builds.
  • Language Specification Alignment: By targeting Java 21, you bridge potential gaps in type inference and bytecode generation in mixed Java/Kotlin projects, especially as Kotlin 2.x gains traction.
  • Precision R8 Optimization: AGP 9 is optimized to parse and transform class files generated by JDK 21. This means that even with stricter settings, R8 can more accurately understand modern code structures, reducing the need for excessive keep rules while safely shrinking code.

This combination offers the kind of seamless experience you get from pairing the latest OS with the latest CPU.

 

🤔 Prepare with gradle.properties: 10 Flags to Enable Today

The strategy is simple: enable one flag at a time, fix any errors that arise, and then move to the next. This iterative approach is the most reliable way to prepare for AGP 9.

1. Structure Enforcement (Clean Up Your Project)

  • android.uniquePackageNames=true: Prevents duplicate package names across modules, eliminating resource conflicts.
  • android.usesSdkInManifest.disallowed=true: Enforces placing minSdk, targetSdk, etc., in build.gradle instead of AndroidManifest.xml.
  • android.defaults.buildfeatures.resvalues=true: Explicitly controls the generation of resValue entries.

2. Build Speed Enhancements

  • android.enableAppCompileTimeRClass=true: Uses lightweight R classes during app compilation, significantly improving build times for large projects.
  • android.sdk.defaultTargetSdkToCompileSdkIfUnset=true: Automatically sets targetSdk to compileSdk if unspecified, preventing inconsistent behavior.
  • android.dependency.useConstraints=true: Utilizes Gradle's "Constraints" feature for dependency resolution, making library version management more robust.

3. Aggressive R8 / Optimization Settings (The Biggest Hurdle)

  • android.r8.strictFullModeForKeepRules=true: Enables R8's Full Mode. This maximizes optimization but requires precise keep rules for code that relies on reflection, potentially leading to crashes if not handled correctly.
  • android.r8.optimizedResourceShrinking=true: Employs a more advanced algorithm for removing unused resources, leading to smaller app sizes.

4. Next-Gen Defaults

  • android.builtInKotlin=true: Prioritizes AGP's built-in Kotlin support.
  • android.newDsl=false: Use this to maintain the current DSL while preparing for future changes.

 

🤔 Conclusion: One Flag at a Time for a Smoother Future

The AGP 9 update is akin to a major cleanup. Attempting it all at once can be overwhelming, but tackling it gradually makes it incredibly rewarding.

Why not start with android.uniquePackageNames=true? With each flag you enable, your project will move closer to a more modern, robust, and efficient build environment.

👉 AGP 9.0 移行ガイド:新旧コード比較で見るモダンビルド設定