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 チャンネルを覗いてみるのがおすすめ。そこでは、より人間的で身近な困りごとや質問が、消化しやすい形で活発に議論されている。


「process death」とは何?

Androidにおける Process Death (プロセス終了) とは、システムがメモリ不足(Memory Pressure)になった際に、バックグラウンドにあるアプリのプロセスを OS が強制的に終了させる仕組みのことです。

ユーザーがアプリを閉じたり明示的に終了させたりする「通常の終了」とは異なり、OS側の都合で実行されるため、適切な対策をしないとアプリに戻った際にデータが消えてしまう原因になります。

 

🤔 なぜ気ににする必要があるのか

長いお問い合わせフォームを入力中に、少し調べ物をして戻ったら全部消えていた。

ECサイトで商品を比較していたのに、トップ画面に戻された。

「このアプリは不安定だ」「使いにくい」と思われ、アンインストールや低評価に直結します。

 

🤔 予期せぬクラッシュを防ぐため

Process Death からの復帰時、OSは「最後に開いていた画面」をいきなり表示しようとします。

もし、その画面が 「前の画面から渡されたデータ(IDなど)」 に依存しているのに、それをメモリ上の変数(ViewModelのフィールドなど)にしか持っていなかった場合、復帰した瞬間にデータが null や空になり、アプリがクラッシュします。

 

🤔 「バックグラウンド=一時停止」ではない

Androidの設計思想では、アプリがバックグラウンドに回った瞬間から、いつ消されても文句は言えないことになっています。

「メモリが潤沢にあるから大丈夫だろう」という想定は、現代のAndroid(特にバックグラウンドで動くサービスや重いゲームが多い環境)では通用しません。

「プロセスは必ずいつか死ぬ」という前提で設計することが、Android開発におけるプロフェッショナルな作法とされています。

 

🤔 開発時の確認方法

以下で開発時に確認しておくのがいいです。


1. アプリを起動したあと、ホーム画面に移動する。(終了させるのではなくて、バックグラウンドに移動する)

2. adb shell am kill <アプリのパッケージ名>

3. アプリの起動履歴から再度アプリを選択する。

注意: am force-stop とは異なり、am kill はバックグラウンドにいるアプリに対して「メモリが足りなくなったからシステムが回収した」という状態をシミュレートします。

起動履歴から再度アプリを起動したときに、バックグラウンドに移動する前の画面と同じものが表示されれば OK です。

 

🤔 まとめ

上記手順でやってみました。

これでOK!!


Jetpack Compose における State と Effect の境界線:ワンショットイベントに Channel を採用する理由

Jetpack Compose で開発をしていると、必ず直面する問いがあります。

「これは State として保持すべきか、それとも Effect(副作用)として処理すべきか?」

という問題です。

Compose の宣言的 UI パラダイムにおいて、この境界線を曖昧にすると、画面回転時の二重トーストや、意図しない画面遷移といったバグを招きます。

本記事では、その明確な使い分けと、イベント制御における Kotlin Channel の有効性について解説します。

 

🧑🏻‍💻 1. 「状態 (State)」と「副作用 (Effect)」の本質的な違い

使い分けの基準はシンプルです。

「そのデータは、UI のスナップショットの一部か?」

と自問してください。

State:UI の「今」を表すもの

State は、再構成(Recomposition)によって何度読み込まれても同じ結果を示すべきものです。

  • 例: テキストフィールドの入力値、読み込み中フラグ、リストデータ
  • 性質: 保持(Retention)

Effect:UI の「外」で起きる一回きりのこと

Effect は、Compose のレンダリングサイクルとは独立して実行される処理です。

  • 例: ログ出力、アナリティクス送信、タイマーの開始
  • 性質: 実行(Execution)

 

🧑🏻‍💻 2. ワンショットイベントの罠:StateFlow vs Channel

ここで問題になるのが、トースト表示や画面遷移のような「一度だけ実行したいアクション」です。

これらを StateFlow で管理しようとすると、Android 特有のライフサイクル問題にぶつかります。

StateFlow の限界

StateFlow は常に「最新の状態」を保持します。

1. エラーが発生し、State を ErrorMessage("Failed") に更新。
2. UI がそれを検知してトーストを表示。
3. ここで画面を回転させる。
4. 新しい Activity が StateFlow を購読し、最新の "Failed" を再び受け取ってしまう。
5. トーストが二重に表示される。

これを防ぐために「フラグを戻す」処理を挟むのは、シンプルではありません。

 

🧑🏻‍💻 3. Channel は「消費されるイベント」に最適である

そこで登場するのが Channel です。Channel は、「土管」のような振る舞いをします。

  • 一度きりの配送: 誰かがイベントを受け取った(消費した)瞬間、そのイベントは Channel から消えます。
  • 画面回転に強い: 新しい Activity が再購読しても、古いイベントは既に消費されているため、二重実行は発生しません。
  • バッファの活用: Channel.BUFFERED を使うことで、アプリがバックグラウンドにいる間に発生したイベントも、フォアグラウンドに戻った瞬間に安全に処理できます。

 

🧑🏻‍💻 4. 実装のベストプラクティス

私のプロジェクトでは、以下のような棲み分けを徹底しています。


// ViewModel

// UI の状態(表示データ)
private val _uiState = MutableStateFlow(UiState())
val uiState = _uiState.asStateFlow()

// UI へのイベント(ワンショット)
private val _eventChannel = Channel<UiEvent>(Channel.BUFFERED)
val events = _eventChannel.receiveAsFlow()


// UI (Compose)

LaunchedEffect(Unit) {
    viewModel.events.collect { event ->
        when (event) {
            is UiEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message)
            is UiEvent.NavigateToDetail -> navController.navigate("detail")
        }
    }
}

 

🧑🏻‍💻 まとめ

  • 永続的な見た目に関わるなら State (StateFlow)
  • 一過性の挙動に関わるなら Effect (Channel)

複雑なフラグ管理でコードを汚す前に、ツールが持つ「自然な性質」を利用しましょう。

Channel を使うことは、Compose におけるイベントハンドリングを最もシンプルにする考え方の一つです。


ViewModelの責務はどう変わった? 3つの図から紐解くアーキテクチャーの変遷

モダンなAndroid開発(Jetpack Compose + MVVM/MVI)において、ViewModelの役割は単なる「データ保持」から「状態管理のハブ」へと進化しました。今回は、3つのアーキテクチャ図を比較しながら、その設計思想の違いを整理します。

 

🤔 1. 最もシンプルな「State管理のみ」


class TaskViewModel(private val repository: TaskRepository) : ViewModel() {
    // 状態(State)のみを保持
    val uiState: StateFlow<List<Task>> = repository.getTasks()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    // 直接関数を呼び出す
    fun addTask(name: String) {
        viewModelScope.launch {
            repository.saveTask(Task(name))
        }
    }
}


@Composable
fun TaskScreen(viewModel: TaskViewModel = viewModel()) {
    // 状態をそのまま収集
    val tasks by viewModel.uiState.collectAsState()
    var text by remember { mutableStateOf("") }

    Column {
        TextField(value = text, onValueChange = { text = it })
        Button(onClick = { 
            // ViewModelの関数を直接叩く (Simple)
            viewModel.addTask(text) 
        }) {
            Text("追加")
        }
        
        LazyColumn {
            items(tasks) { task -> Text(task.name) }
        }
    }
}

特徴:
ViewModelはRepositoryからデータを取得し、State(状態)を保持してViewへ流すだけのシンプルな構造です。

メリット:
コード量が少なく、小規模なプロジェクトや単純な画面には最適。

課題:
ビジネスロジックがViewModelに肥大化しやすく(Fat ViewModel)、View側でのユーザー操作(Action)がどう処理されるかが図示されておらず、双方向のやり取りが曖昧になりがちです。

 

🤔 2. Domain/Usecaseの導入による責務の分離


// ビジネスロジックを分離
class GetSortedTasksUseCase(private val repository: TaskRepository) {
    operator fun invoke(): Flow<List<Task>> = repository.getTasks().map { it.sortedBy { t -> t.date } }
}

class TaskViewModel(
    private val getSortedTasksUseCase: GetSortedTasksUseCase,
    private val addTaskUseCase: AddTaskUseCase 
) : ViewModel() {

    val uiState = getSortedTasksUseCase()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

    fun onAddTaskClicked(name: String) {
        viewModelScope.launch {
            addTaskUseCase(name)
        }
    }
}


@Composable
fun TaskScreen(viewModel: TaskViewModel = viewModel()) {
    val tasks by viewModel.uiState.collectAsState()

    // UIロジック(ソート済みデータなど)を表示するだけ
    TaskList(
        tasks = tasks,
        onAddClick = { name -> viewModel.onAddTaskClicked(name) }
    )
}

特徴:
ViewModelの中にDomain/Usecaseという概念が登場します。

進化のポイント:
ビジネスロジックをRepository直呼びではなく、Usecaseとして切り出すことで、ViewModelは「UIの状態管理」に専念できるようになります。

メリット:
ロジックの再利用性が高まり、ユニットテストが書きやすくなります。クリーンアーキテクチャに近い考え方です。

 

🤔 3. MVI(Intent/Action)とEffectの導入


// 1. 入力(Action)と出力(Effect)を型定義
sealed interface TaskAction {
    data class AddTask(val name: String) : TaskAction
    object Refresh : TaskAction
}

sealed interface TaskEffect {
    data class ShowSnackBar(val message: String) : TaskEffect
    object NavigateToDetail : TaskEffect
}

class TaskViewModel(private val useCase: TaskUseCase) : ViewModel() {
    private val _effects = Channel<TaskEffect>()
    val effects = _effects.receiveAsFlow()

    // 2. Actionを一箇所で受ける (Intent)
    fun dispatch(action: TaskAction) {
        when (action) {
            is TaskAction.AddTask -> handleAddTask(action.name)
            TaskAction.Refresh -> { /* リフレッシュ処理 */ }
        }
    }

    private fun handleAddTask(name: String) {
        viewModelScope.launch {
            useCase.add(name)
            // 3. 状態変化ではない「副作用」を通知
            _effects.send(TaskEffect.ShowSnackBar("保存しました"))
        }
    }
}


@Composable
fun TaskScreen(viewModel: TaskViewModel = viewModel()) {
    val tasks by viewModel.uiState.collectAsState()
    val snackbarHostState = remember { SnackbarHostState() }

    // 1. 副作用(Effect)のハンドリング
    LaunchedEffect(Unit) {
        viewModel.effects.collect { effect ->
            when (effect) {
                is TaskEffect.ShowSnackBar -> {
                    snackbarHostState.showSnackbar(effect.message)
                }
                TaskEffect.NavigateToDetail -> {
                    // Navigation 3 等での遷移処理
                }
            }
        }
    }

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) { padding ->
        TaskContent(
            tasks = tasks,
            onAction = { action -> 
                // 2. すべての操作を dispatch(Action) に集約
                viewModel.dispatch(action) 
            }
        )
    }
}

@Composable
fun TaskContent(tasks: List<Task>, onAction: (TaskAction) -> Unit) {
    Button(onClick = { onAction(TaskAction.AddTask("New Task")) }) {
        Text("Actionを送る")
    }
}

特徴:
ユーザーの操作をAction/Intentとして定義し、副作用をEffectとして分離した、最もモダンな形です。

進化のポイント:

  • Action/Intent: Viewからの入力が型定義されたイベントとしてViewModelに届く(単方向データフローの強化)。
  • Effect: 画面遷移やトースト表示など、一回限りのイベントをStateとは別に管理する。

メリット:
状態遷移が予測可能になり、デバッグが容易になります。Composeとの相性も抜群です。

 

🤔 まとめ:どれを選ぶべきか?

最初からパターン3を詰め込みすぎると複雑になりますが、長期的な保守を考えるなら、たとえ小規模でも「ViewからのAction」と「UIへのState」を明確に分ける意識が大切です。

まずは「何がViewに属し、何がロジックに属するか」をシンプルに保つことが、良いアーキテクチャへの近道です。


RxJavaすら使わない。Androidに潜む「古代Java」の亡霊たち

JavaエンジニアがKotlinに移行する際、最も危険なのは「Kotlinの文法でJava5の頃の思考で書く」ことです。

RxJava(リアクティブプログラミング)という高い壁を飛び越えようとして、逆に20年前の古典的手法に着地してしまうケースが後を絶ちません。


fun loadUser(callback: (User?) -> Unit) {
    api.getUser { user ->
        if (user != null) {
            database.save(user) {
                cache.update(user) {
                    analytics.track(user) {
                        callback(user)
                    }
                }
            }
        } else {
            callback(null)
        }
    }
}


interface OnUserLoadedListener {
    fun onLoaded(user: User)
}

fun loadUser(listener: OnUserLoadedListener) {
    api.getUser(object : ApiCallback {
        override fun onSuccess(user: User) {
            database.save(user, object : SaveCallback {
                override fun onSaved() {
                    listener.onLoaded(user)
                }
            })
        }

        override fun onError() {
        }
    })
}

 

🤔 1. RxJava以前の「古代遺物」がモダンなKotlinを侵食する

RxJavaすら導入されていない現場、あるいは「Rxは難しいから」と避けた結果、以下のような絶滅危惧種がKotlinの皮を被って出現します。

① 独自インターフェースによる「バケツリレー」

interface MyCallback を定義し、それを Activity から Presenter(あるいは ViewModel)、さらに Repository へと引数で渡していくスタイルです。

地獄のポイント: 1つの処理を追うのに3つ以上のファイルを跨ぐ必要があり、デバッグ中に「今どこにいるのか」を見失います。

② AsyncTask の「自力再実装」

Googleが非推奨にした AsyncTask ですら、中身はスレッド管理とコールバックの塊でした。これをKotlinの Thread { ... }Handler(Looper.getMainLooper()) で自作再現してしまうパターンです。

地獄のポイント: isDestroyed のチェックを忘れ、画面を閉じた後にクラッシュ(NullPointerException)させる「爆弾」を量産します。

③ MutableList を使った共有メモリの恐怖

非同期の戻り値を待てず、外部の MutableList に値を詰め込ませ、別の場所で TimerThread.sleep を使って「値が入ったか監視する」という、スレッドセーフを無視した力技です。

 

🤔 2. なぜ「古代手法」は再生産されるのか?

それは、Javaエンジニアが長年培ってきた「命令型プログラミング」の呪縛です。

「待つ」という概念の欠如: 「処理を止めたらスレッドが死ぬ(UIが固まる)」という恐怖心から、すべてを「終わったらこれを呼べ」という受動的な構造(ハリウッド原則)にしてしまいます。

状態管理の煩雑さ: 古いJavaでは、状態の変化を「通知」する仕組みが乏しかったため、泥臭いフラグ管理やコールバックに頼らざるを得ませんでした。

 

🤔 3. 歴史の授業:非同期処理の進化系統図

今のAndroid開発者が知っておくべき、技術の「地層」は以下の通りです。

 

🤔 4. まとめ:レガシーの鎖を断ち切るために

Javaエンジニアの皆さんが持つ「堅牢なクラス設計」の知識は宝です。しかし、「非同期処理の書き方」だけは、一度全て忘れてください

Kotlinにおける suspend は、ただのキーワードではありません。それは、私たちが10年以上苦しめられてきた「コールバック地獄」という名の迷宮から脱出するための、唯一の出口なのです。