【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の第一歩)

ぐらいか。


The New Standard in Android Studio Panda: Automating JDK Management with Foojay Resolver

As an Android developer, are you still wasting time managing JDK versions?

"I cloned a new project and the build failed,"
"Updating JDK settings in CI is a pain,"
"Different team members are using different JDK vendors..."

These headaches are now a thing of the past thanks to the combination of Android Studio Panda (2025.3.1), AGP 9.1, and the Foojay Resolver plugin.

 

🤔 1. What is org.gradle.toolchains.foojay-resolver-convention?

In short, it is a plugin that allows Gradle to automatically find, download, and configure the required JDK from the internet.

Normally, even if you define a Java Toolchain in your build.gradle, the build will fail if that specific JDK isn't already installed on your local machine.

By adding this plugin, Gradle communicates with the Foojay (Friends Of OpenJDK) database (via the Disco API) to automatically fetch and set up the correct JDK for you.

 

🤔 2. What Changed in Android Studio Panda?

With the release of Android Studio Panda, JDK management has shifted from "IDE-driven" to "Project-driven (Gradle-driven)."

  • Gradle Daemon JVM Criteria: Instead of manually selecting a JDK in the IDE settings, Android Studio now reads the toolchain configuration directly from your project files. It automatically switches the JVM used to run Gradle itself (the Daemon) to match your project.
  • Synchronized Environment: This eliminates the common "it works in the terminal but fails in the IDE" issue. The JDK used by ./gradlew and the "Run" button in Android Studio will now always be 100% identical.

 

🤔 3. Critical Notes for AGP 9.1

If you are using AGP 9.1 or higher, keep these points in mind:

  • Java 21 Requirement: AGP 9.x series strictly requires JDK 21.
  • Consistency is Key: Since AGP 9.1 strongly encourages the Gradle Daemon and the compilation JVM to be the same, the benefits of automatic resolution via foojay-resolver are more significant than ever. It ensures your entire pipeline stays on JDK 21 without manual intervention.

 

🤔 4. Implementation Guide (Quick Steps)

Step 1: Update settings.gradle.kts

Add the plugin to the very top of your root settings.gradle.kts file. This enables the automatic download capability.


plugins {
    // The magic line for automatic JDK downloads
    id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}

Step 2: Configure build.gradle.kts

Define the Java version in your app module’s build.gradle.kts (or within a convention plugin).


android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_21
        targetCompatibility = JavaVersion.VERSION_21
    }
    
    kotlinOptions {
        jvmTarget = "21"
    }

    // Java Toolchain configuration
    java {
        toolchain {
            languageVersion.set(JavaLanguageVersion.of(21))
            // Optional: Specify a vendor if needed
            // vendor.set(JvmVendorSpec.ADOPTIUM)
        }
    }
}

 

🤔 5. Key Benefits at a Glance

 

🤔 Conclusion

In the era of Android Studio Panda and AGP 9.1, foojay-resolver-convention is no longer just a "nice-to-have" option—it is core infrastructure for modern Android development.

When upgrading your legacy projects, make this plugin your first priority. Stop fighting with environment variables and start focusing on what matters most: writing great code.

[!TIP] To verify that your JDKs are being recognized correctly, run ./gradlew -q javaToolchains in your terminal.