Navigation3 時代のSavedStateHandleを考え直す

Jetpack Navigation 3が登場し、画面遷移の設計は大きく変わろうとしています。

その中でも特に変化が大きいのが、ViewModelでSavedStateHandleから画面引数を取得するという、これまで当たり前だった実装です。


@HiltViewModel 
class UserViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle
) : ViewModel() { 
    val userId: String = checkNotNull(savedStateHandle["userId"]) 
}

これまで多くのサンプルや公式ドキュメントでも紹介されてきたため、この書き方に違和感を持つ人は少ないでしょう。

しかしNavigation 3では、この設計を前提としなくてもよくなりました。

むしろ、画面引数は直接ViewModelへ渡すほうが自然だという考え方へ移りつつあります。

 

🧑🏻‍💻 SavedStateHandleは本来何のためのAPIなのか

SavedStateHandleは、プロセスデス時の状態復元を目的として作られたAPIです。

Androidではメモリ不足になるとアプリのプロセスが終了されます。

その際、入力途中の内容やスクロール位置などをBundleへ保存し、アプリ復元時に元へ戻せるようにする仕組みがあります。

SavedStateHandleは、その保存領域へアクセスするためのラッパーです。

つまり、本来の役割は

- テキスト入力
- スクロール位置
- 一時的なUI状態

などを保持することであり、

画面引数を受け渡すためのAPIではありません。

 

🧑🏻‍💻 Navigation 2では便利だった

Navigation Composeでは画面引数が自動的にSavedStateHandleへ入るため、


val userId = savedStateHandle["userId"]

だけで取得できました。

非常に便利だった反面、この実装には問題もありました。

ViewModelが

- 引数名
- Navigationの仕様
- ルート構造

まで知る必要があったからです。

本来ビジネスロジックだけを持つはずのViewModelが、Navigationへ依存してしまいます。

 

🧑🏻‍💻 Navigation 3では考え方が変わった

Navigation 3では画面遷移が型安全になりました。

例えば


@Serializable
data class UserScreen(
    val userId: String
)

のようにルート自体がオブジェクトになります。

Composableでは


fun UserScreen(
    route: UserScreen
)

として受け取れるため、

ViewModelにもそのまま渡せます。


@HiltViewModel(assistedFactory = Factory::class)
class UserViewModel @AssistedInject constructor(
    @Assisted
    private val route: UserScreen
) : ViewModel()

これなら

- 文字列キーが不要
- 型安全
- Navigationへの依存が少ない

というメリットがあります。

 

🧑🏻‍💻 ViewModelはNavigationを知らなくていい

以前は


Navigation
    ↓
SavedStateHandle
    ↓
ViewModel

という流れでした。

Navigation 3では


Navigation
    ↓
Route
    ↓
ViewModel

という構成になります。

ViewModelは必要なデータだけを受け取り、Navigationの実装を意識しなくて済みます。

責務も明確になります。

 

🧑🏻‍💻 SavedStateHandleが不要になったわけではない

もちろん、SavedStateHandle自体が不要になるわけではありません。

例えば

- 検索キーワード
- フォーム入力
- スクロール位置
- タブ選択状態

など、プロセスデス後も復元したいUI状態には引き続き最適です。

つまり、

- 画面引数
- UI状態

を分けて考えることが重要になります。

 

🧑🏻‍💻 これからの設計

Navigation 2では

「画面引数=SavedStateHandle」

という書き方が一般的でした。

しかしNavigation 3では、

- Routeをそのまま渡す
- Assisted Injectionを利用する
- SavedStateHandleはUI状態だけに使う

という設計のほうがシンプルで責務も明確になります。

すぐに既存コードを書き換える必要はありませんが、新規実装ではこの考え方を取り入れていく価値は十分にあるでしょう。

Navigation 3は単なるAPIの更新ではなく、ViewModelとNavigationの関係そのものを見直すきっかけになりそうです。

 

🧑🏻‍💻 参考


Jetpack Composeで snapshotFlow の使いどころ

Jetpack Composeにおいて、Composeの「状態(State)」と「非ComposeのAPI(Flowなど)」を連携させたい場合に snapshotFlow が非常に有効です。

具体的には、以下のようなケースで使用します。

 

🧑🏻‍💻 1. Composeの状態変化をFlowとして監視したいとき

Composeの State オブジェクトの値が変化したタイミングで、何らかの副作用(Side-effect)を起こしたい場合に使用します。

コード例:TextFieldの入力値を監視して検索APIを呼ぶ


LaunchedEffect(textState) {
    snapshotFlow { textState.value }
        .debounce(300L) // 300ms入力が止まったら処理
        .distinctUntilChanged()
        .collect { query ->
            viewModel.search(query)
        }
}

 

🧑🏻‍💻 2. 非ComposeのAPIと値を同期させたいとき

Composeの管理外にあるライブラリやシステムAPIに対し、Compose側の状態の変化を伝えたい場合です。

コード例:スクロール位置をトラッキングツールへ送信


LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .filter { it > 0 }
        .collect { index ->
            Analytics.log("ScrollPosition", index)
        }
}

 

🧑🏻‍💻 3. 複雑な条件で「状態の変化」をトリガーにしたいとき

LaunchedEffect は状態が更新されるたびに毎回実行されますが、snapshotFlow を使うと、

Flowの演算子(filter, distinctUntilChanged など)を組み合わせる

ことで、「特定の値になったときだけ」といった細かい条件付けが容易になります。

 

🧑🏻‍💻 注意点:使うべきではないケース

すべての状態監視に snapshotFlow を使う必要はありません。

- 単純なUIの更新: 単にComposeのUIを再描画したいだけであれば、snapshotFlow を使わず、Composeの State をそのまま読み取ってUIを構成するのが最も効率的です。

- 計算処理: 高負荷な計算をFlowの中で行う場合は、適切なスレッド(Dispatchers.Defaultなど)で処理を行うよう注意が必要です。

 

🧑🏻‍💻 まとめ

snapshotFlow は、

「Composeのリアクティブな状態」を「Kotlin Flowの強力な演算子」とブリッジさせるためのツール

です。

「状態が変化した時に、何か別のアクション(通信、ログ記録、画面遷移など)を発生させたい」と考えたときが、この機能の使いどころです。

 

🧑🏻‍💻 参考


2026 FIFAワールドカップを無料で視聴できる国まとめ

2026 FIFAワールドカップは史上最多となる48カ国が参加し、全104試合が開催されます。

日本では有料配信や放送形態がまだ確定していない部分もありますが、海外に目を向けると、多くの国で無料視聴が予定されています。

今回は、2026 FIFAワールドカップを無料で視聴できる主要な国と放送局をまとめました。

 

🤔 無料で視聴できる主な国

イギリス

BBCとITVが全104試合を分担して放送予定です。

両局とも無料放送で知られており、イギリス国内ではワールドカップのほぼ全試合を追加料金なしで楽しめます。

オーストラリア

SBSが大会全試合を無料配信する予定です。

インターネット経由でも視聴できるため、近年のスポーツ中継では非常に人気の高い選択肢となっています。

ブラジル

ブラジルではCazéTVが注目されています。

YouTubeを活用したスポーツ配信で急成長しており、2026年大会でも全試合の無料配信が予定されています。

ヨーロッパ各国

オランダのNOS、ベルギーのVRT・RTBF、スイスのSRF・RTS・RSI、アイルランドのRTÉなど、多くの公共放送局が無料中継を実施する見込みです。

ヨーロッパではサッカー人気が非常に高く、ワールドカップを無料で視聴できる国が数多く存在します。

中国

中国ではCCTVが無料放送を行う予定です。

テレビだけでなくオンライン配信にも対応する可能性があります。

 

🤔 海外の無料配信を見る方法

海外放送局の多くは配信地域を制限しています。

そのため、日本から直接アクセスしても視聴できない場合があります。

一般的には以下のような仕組みになっています。


視聴者
   ↓
インターネット
   ↓
放送局の配信サーバー
   ↓
地域判定
   ├─ 対象国 → 視聴可能
   └─ 対象外 → 視聴不可

配信条件や権利契約は大会直前に変更されることもあるため、最新情報の確認が重要です。

 

🤔 まとめ

2026 FIFAワールドカップでは、多くの国で無料放送・無料配信が予定されています。

特にイギリスのBBC・ITV、オーストラリアのSBS、ブラジルのCazéTVは、全試合を無料で視聴できる有力な選択肢として注目されています。

大会が近づくにつれて放映権情報は更新されるため、各放送局やFIFAの公式発表を定期的に確認しておきましょう。

 

🤔 参考リンク

- Tom's Guide — How to watch World Cup 2026: live stream every game for FREE
https://www.tomsguide.com/entertainment/sports/watch-world-cup-2026-free-live-streams

- FIFA World Cup 2026
https://fifa-2026.com/fifa-2026-broadcasting-and-streaming

- 2026 FIFA World Cup broadcasting rights
https://www.fifaworldcupnews.com/fifa-world-cup-2026-broadcasting-rights/


Jetpack Compose Navigation3 複数 NavBackStack 超シンプル実装

公式サンプルは複雑すぎない?

と思ったので。

 

🤔 実際のアプリはボトムナビゲーションをどう扱っているか

実装に入る前に、主要アプリの挙動を見てみましょう。

ほとんどのアプリは以下のどれかです:

- タブごとに独立した BackStack(最も一般的)
- 1つの共有 BackStack(最もシンプル)
- タブ切り替え時にリセット(常にルートに戻る)

 

🤔 バックボタンの挙動


タブ内の履歴

  ↓

タブのルート

  ↓

アプリ終了

- タブの切り替えは手動(バックボタンでは行わない)
- 各タブは独自の履歴を持つ
- 履歴が空になったらアプリを終了
- 同じタブを再度タップ → そのタブをルートにリセット
- タブ数は固定

 

🤔 考え方

タブをキーとした BackStack の Map を使う

これだけです。複雑な状態ホルダーは一切不要です。

 

🤔 主要コンポーネント

- NavKey
- NavBackStack
- NavigationBar / NavigationBarItem
- NavDisplay

 

🤔 ナビゲーションモデル


NavKey
└── TabRoot (エントリポイント)
     ├── Home
     ├── Search
     │    ├── Result (検索結果画面)
     │    └── Detail (検索詳細画面)
     └── Profile



@Serializable
sealed interface TabRoot : NavKey {
    val label: String
    val selectedIcon: ImageVector
    val unselectedIcon: ImageVector

    companion object {
        val entries = listOf(Home, Search, Profile)
    }
}

@Serializable
data object Home : TabRoot {
    override val label = "Home"
    override val selectedIcon = Icons.Filled.Home
    override val unselectedIcon = Icons.Outlined.Home
}

@Serializable
data object Search : TabRoot { ... }

@Serializable
data object Profile : TabRoot { ... }

@Serializable
data class Result(val keyword: String) : NavKey

@Serializable
data class Detail(val id: String) : NavKey

なぜこれがうまくいくか

- 単一の型安全なナビゲーションモデル
- タブと画面が同じシステムを共有
- @Serializable で状態復元が可能
- sealed + object でコンパイル時安全性

 

🤔 状態管理

1. 選択中のタブ


var currentTab by rememberSerializable {
    mutableStateOf<TabRoot>(Home)
}


var currentTab by rememberSerializable {
    mutableStateOf<TabRoot>(Home)
}

2. 複数の NavBackStack


val stacks = TabRoot.entries.associateWith { root ->
    rememberNavBackStack(root)
}

重要なポイント
- Map は再コンポーズごとに再作成されが生成コストは小さい
- しかし各 NavBackStack 維持されて再作成されない

これで安全かつシンプルに実現できます。

3. 現在の NavBackStack


val currentStack = stacks[currentTab]!!

Compose では currentTab が変わると自動的に更新されます。

4. タブ切り替え


onClick = {
    currentTab = root
}

これだけです。

5. 画面遷移(Push)kotlin


onClick = {
    currentStack.add(Result(keyword))
}

6. 戻る操作(Pop)


NavDisplay(
    onBack = {
        currentStack.removeAt(currentStack.lastIndex)
    }
)

- ルートを削除しようとしたらアプリ終了(Android 標準挙動)

7. 同じタブを再度タップしたときのリセット


if (selected) {
    currentStack.clear()
    currentStack.add(root)
}

- Instagram や Twitter(X)と同じ挙動になります。

 

🧑🏻‍💻 最終的な最小パターン


var currentTab by rememberSerializable {
    mutableStateOf<TabRoot>(Home)
}

val stacks = TabRoot.entries.associateWith { root ->
    rememberNavBackStack(root)
}

val currentStack = stacks[currentTab]!!

 

🚀 このアプローチが優れている理由

- コード量が最小
- 実際のアプリ挙動にマッチ
- 完全に Compose ネイティブ
- 将来的に拡張しやすい
- Process Death 後も完全な状態復元が可能

というかんじでどうでしょうか。

 

🚗💨 参考


【Kotlin 2.4】ついに登場!コレクションリテラル(実験的サポート)と型推論の仕組みを徹底解説

Kotlin開発者の皆さん、お待たせしました!

2026年6月にリリースされた Kotlin 2.4 にて、待望の「コレクションリテラル(Collection Literals)」が実験的(Experimental)にサポートされました。

これまで listOf()mutableListOf()arrayOf() などの関数を使って記述していた配列やリストの生成が、ついにスクエアブラケット [] を使って、よりシンプルかつ直感的に書けるようになります。

この記事では、コレクションリテラルの導入方法から、最も重要な挙動である「文脈依存の型推論(Context-sensitive Type Inference)」について詳しく解説します。

 

🧑🏻‍💻 コレクションリテラルとは?

Kotlin 2.4.0からは、以下のようにブラケット [] を使ってコレクション(配列やリストなど)を簡潔に作成できるようになります。


// Kotlin 2.4からの新しい書き方(リテラル表記)
val names = ["Joe", "Alice"]

従来の listOf("Joe", "Alice") と比べてタイピング量が減り、他言語(Javaの配列リテラルや、JavaScript/TypeScript/Pythonなどの配列・リスト表記)に慣れている開発者にとっても、より親しみやすいコードになります。

 

🧑🏻‍💻 導入方法(実験的サポートの有効化)

Kotlin 2.4時点では、この機能はまだ実験的(Experimental)な位置づけです。そのため、プロジェクトで利用するにはコンパイラオプションで明示的に機能を有効化する必要があります。

build.gradle.kts に以下の設定を追加してください。


kotlin {
    jvmToolchain(21)
    compilerOptions {
        // コレクションリテラルを有効化するコンパイラ引数
        freeCompilerArgs.add("-Xcollection-literals")
    }
}

 

🧑🏻‍💻 注目すべき「型推論」の挙動

コレクションリテラルを使う上で、最も興味深いのが「コンパイラがどのように型を推論するか」という点です。

Kotlinのコレクションリテラルは、単一の固定された型を表すのではなく、周囲の文脈(期待される型)に応じて最終的な型が変化する「文脈依存(Context-sensitive)」の性質を持っています。

具体的なコードで比較してみましょう。

1. 明示的な型指定がない場合

ターゲットとなる型を何も指定せずにリテラルを書いた場合、コンパイラはデフォルトで Array(配列) と推論します。


val names = ["Joe", "Alice"]
// 推論される型: Array<String>

2. 期待される型(型注釈)がある場合

変数の型を明示的に指定すると、リテラルの中身は同じ [] であっても、コンパイラが文脈を読み取って適切なコレクション型へと変換してくれます。


val names: MutableList<String> = ["Joe", "Alice"]
// 推論される型: MutableList<String>

このように、左辺の MutableList<String> という情報をコンパイラが解釈し、[] の部分を適切に MutableList として扱ってくれるのが、今回の型推論の面白いところです。

 

🧑🏻‍💻 まとめ:Kotlinのコードはさらに洗練される

Kotlin 2.4のコレクションリテラルは、ただの「書き方の省略」にとどまらず、Kotlinの強力な型推論エンジンとシームレスに融合している点が大きな特徴です。

現在はまだ実験的な機能であるため、プロダクション環境への導入には慎重になる必要がありますが、将来的に正式機能となれば、Kotlinのコードをさらにモダンでスッキリとしたものに変えてくれることは間違いありません。

興味のある方は、ぜひコンパイラオプションを追加して、手元のプロジェクトで新しい書き味を試してみてください!