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年以上苦しめられてきた「コールバック地獄」という名の迷宮から脱出するための、唯一の出口なのです。


【Jetpack Compose】mutableStateOf(list) と mutableStateListOf() の違いと使い分け

Jetpack Composeでリストを扱う際、多くの開発者が最初にぶつかる壁が
「mutableStateOf(list) と mutableStateListOf() どっちを使えばいいの?」
という疑問です。

この記事では、両者の型、構造、そして「なぜあの書き方は動かないのか?」という落とし穴についてスッキリ整理して解説します。

 

🤔【結論】一目でわかる比較表

 

🤔 mutableStateOf(listOf<T>())

型の構造:箱ごと入れ替えるスタイル

mutableStateOf は「値(value)」そのものを監視します。リストを扱う場合、中身は不変(Immutable)な List であることが前提です。

型 :
MutableState<List<T>>

変更方法:
state.value = state.value + "A"

リストの中身をいじるのではなく、「新しいリストを作成して、Stateという箱に入れ直す」ことでComposeに通知します。

 

🤔 mutableStateListOf<T>()

型の構造:中身の動きを監視するスタイル

mutableStateListOf は、それ自体が MutableList のように振る舞い、内部の要素が追加・削除されたことを Compose に直接伝えます。

型 :
SnapshotStateList<T>

変更方法:
list.add("A"), list.remove("B")

リスト自体を再代入する必要はありません。addremove を実行するだけで、Compose が自動的に変更を検知して再描画してくれます。

 

🤔 実務での使い分け

mutableStateOf(list) を使うケース

主に UI State をデータクラスで一括管理する場合です。

・ Flow から combine して State を生成する
data class を用いた一方向データフロー(UDF)設計


data class MyUiState(
    val items: List<String> = emptyList()
)

// ViewModelなどで管理
var uiState by mutableStateOf(MyUiState())

mutableStateListOf() を使うケース

リストそのものが「動的な操作の主体」である場合です。

Navigation3rememberNavBackStack()
・ 要素の追加・削除が頻繁に発生する編集画面やスタック操作

 

🤔 まとめ

基本的には
「UI全体の状態管理なら mutableStateOf」
「リスト個別の動的操作なら mutableStateListOf」
と使い分けるのがスマートです。


Jetpack Compose Navigation3:rememberNavBackStack で実現する宣言的ナビゲーション

Jetpack Compose の次世代ナビゲーションライブラリ Navigation3 では、従来の NavHost と文字列ベースのルート定義から脱却し、より "Compose らしい" 状態管理へと進化しました。

その中核を担うのが rememberNavBackStack です。今回は、この新しい API の「型」の意味と、それを使った最小構成のサンプルコードを解説します。

 

🧑🏻‍💻 rememberNavBackStack の型とその正体

まず、この関数が何を返しているのかを確認しましょう。


val backStack: SnapshotStateList<T> = rememberNavBackStack(initialBackStack)

この関数が返す型は、Compose 独自の SnapshotStateList です。

 

🧑🏻‍💻 なぜこの型なのか?

オブザーバブル(監視可能): SnapshotStateList は、リストの中身(要素の追加や削除)が変化したことを Compose のランタイムに通知します。これにより、スタックを操作した瞬間に NavDisplay が自動的に再描画されます。

保存と復元: rememberNavBackStack は内部で rememberSaveable の仕組みを利用しています。つまり、画面回転やプロセスの再起動が発生しても、ナビゲーションの履歴(スタック)が消えずに保持されることを意味します。

直感的な List 操作: MutableList インターフェースを継承しているため、開発者は add()removeLast()clear() といった標準的なメソッドで遷移をコントロールできます。

 

🧑🏻‍💻 実装サンプル

1. 画面(デスティネーション)の定義
スタックに積むデータ型を sealed interface で定義します。


sealed interface Screen {
    data object Home : Screen
    data class Details(val id: String) : Screen
}

2. ナビゲーションの構築


@Composable
fun MyNavigationApp() {
    // initialBackStack で最初の画面を指定
    // 戻り値はスタックの状態を保持する SnapshotStateList<Screen>
    val backStack = rememberNavBackStack(initialBackStack = listOf(Screen.Home))

    NavDisplay(
        backstack = backStack,
        onBack = { 
            // スタックが2つ以上あれば、最後の要素を取り除いて「戻る」
            if (backStack.size > 1) {
                backStack.removeLast() 
            }
        }
    ) { screen ->
        when (screen) {
            is Screen.Home -> HomeScreen(
                onNavigateToDetails = { id -> 
                    // 1. 新しい画面をスタックに add するだけ
                    backStack.add(Screen.Details(id)) 
                }
            )
            is Screen.Details -> DetailsScreen(
                id = screen.id,
                onBack = { 
                    // 2. 現在の画面を remove するだけで戻れる
                    backStack.removeLast() 
                }
            )
        }
    }
}

 

🧑🏻‍💻 まとめ

Navigation3 において、rememberNavBackStack は単なる「履歴リスト」ではなく、「アプリの現在の状態そのもの」を管理するハブです。

型安全: 任意のオブジェクト(Screen)をスタックに積める。

宣言的: スタックの状態が変われば、UI(NavDisplay)が同期して変わる。

堅牢: SnapshotStateList により、構成変更(回転など)にも強い。

これまでの NavController による命令的な遷移から、「スタックというデータを操作する」という Compose 本来の作法へ。Navigation3 は開発体験を大きくシンプルにしてくれます。


Navigation3 時代の Destination 設計:sealed interface による型安全な実装パターンと使い分け

モダンな Android 開発において、Navigation はもはや単なる「画面の切り替え機」ではありません。

Destinationは、UIの状態やラベル、アイコンといったメタ情報を内包した、純粋な「型」として定義されるべきです。

ここでは、最新の Navigation ライブラリが目指す方向性に沿った、sealed interface による Destination 設計を提案します。

「シンプルさと拡張性」

このトレードオフをどう乗り越えるか、具体的なコード例と共に見ていきましょう。

 

🤔 共通の考え方:Destination = 型 + UIメタ情報

これまでの Navigation では String ベースの Route 管理が主流でしたが、これからの設計は

「型そのものに UI のメタ情報(ラベルやアイコンなど)を持たせる」

のが基本スタイルになります。

 

🤔 パターン 1:ネストする sealed interface

すべての Destination を一つの親インターフェースの中に閉じ込めるスタイルです。

実装イメージ

NavHost では AppDestination.xxx という形で指定します。

特徴

  • ◎ 視認性: 全ての画面遷移先が 1 ファイルにまとまっており、全体像を把握しやすい。
  • ◎ シンプル: 小〜中規模のアプリであれば、管理コストが最小限で済みます。
  • △ 拡張性: 全てが AppDestination に依存するため、機能(Feature)ごとにモジュールを分割しようとすると、循環参照が発生しやすくなります。

 

🤔 パターン 2:ネストしない(トップレベル) sealed interface

インターフェースを定義しつつ、各 Destination は独立したクラスとして定義するスタイルです。

実装イメージ

NavHost での記述はよりフラットになります。

特徴

  • ◎ 疎結合: 各 Destination を別ファイルや別モジュールに切り出しやすいため、Feature 単位の分割に強い。
  • ◎ 大規模向き: チーム開発でコンフリクトを避けやすく、ビルド速度向上のためのマルチモジュール化にも適しています。
  • △ 記述量: クラス名が重複しないよう xxxDestination と命名する必要があり、少し冗長に感じることがあります。

 

🤔 どちらを選ぶべきか?

設計の選択基準は非常にシンプルです。

 

🤔 まとめ

Navigation3 時代の Destination 設計の肝は
「型自体にメタ情報を持たせること」
です。

  • とりあえず作り始めるなら「ネスト型」
  • 将来的な機能拡張やモジュール化を見越すなら「非ネスト型」

アプリの規模と、将来どこまで成長させるかに合わせて選んでみてください。


Jetpack Compose Foundation サンプル目次リンク

Jetpack Composeの核心を担う androidx.compose.foundation

そのサンプルコード群は、Googleのエンジニアが「正しい書き方」を提示している宝庫です。

今回は、これらを実務での利用頻度とモダンな設計(2026年現在のトレンド)に基づいてグループ分けしました。

 

🧑🏻‍💻 1. インタラクション & ジェスチャー(操作感のキモ)

ユーザーが画面に触れた時の挙動を制御する、最も重要なグループです。

 

🧑🏻‍💻 2. スクロール & リスト(データの表示)

効率的にスクロールさせるためのテクニック集です。

 

🧑🏻‍💻 3. テキスト & 入力(文字の表示と編集)

2026年のトレンドである「次世代入力」が含まれます。

 

🧑🏻‍💻 4. 描画 & 視覚効果(見た目のクオリティ)

 

🧑🏻‍💻 5. 高度なシステム統合・同期

 

🧑🏻‍💻 これだけは読んでおくべきトップ5

1. ClickableSamples.kt(すべての基本)
2. LazyDslSamples.kt(リスト表示の要)
3. AnchoredDraggableSample.kt(モダンなUIに必須)
4. BasicTextFieldSamples.kt(入力の実装)
5. CanvasSamples.kt(カスタムUIの第一歩)

ぐらいか。