【Jetpack Compose】スクロール位置を正しく復元する

スクロール位置の復元は、一見するととても簡単そうに見えます。


val gridState = rememberLazyStaggeredGridState( 
    initialFirstVisibleItemIndex = savedIndex, 
    initialFirstVisibleItemScrollOffset = savedOffset
)

しかし、この方法が正しく動作するのは、すでにアイテムのレイアウトが完了している場合だけです。

データを非同期で読み込む画面では、LazyVerticalStaggeredGrid は最初はアイテム数が 0 の状態で生成されることが多くあります。

そのため、指定した初期スクロール位置は反映されません。

この問題を解決するため、多くの開発者は LaunchedEffect の中で scrollToItem() を呼び出します。


LaunchedEffect(Unit) { 
    gridState.scrollToItem(savedIndex, savedOffset)
}

しかし、これにも問題があります。

LaunchedEffect はコンポーズ直後に実行されるため、レイアウトがまだ完了していないタイミングで scrollToItem() が呼ばれてしまう可能性があります。

 

🧑🏻‍💻 グリッドの準備が完了するまで待つ

レイアウトの完了タイミングを推測するのではなく、目的のアイテムが実際にレイアウトされるまで待機するのが確実です。


LaunchedEffect(Unit) { 
    val savedPosition = viewModel.savedScrollPosition

    // 目的のアイテムがレイアウトされるまで待機 
    snapshotFlow { gridState.layoutInfo.totalItemsCount }
        .first { it > savedPosition.index }
 
    gridState.scrollToItem(savedPosition.index, savedPosition.offset) 
}

このコードでは、layoutInfo.totalItemsCount が保存していたインデックスより大きくなるまで処理を一時停止します。

つまり、復元したいアイテムが実際にレイアウトされたことを確認してから scrollToItem() を実行するため、タイミングに依存せず、安定してスクロール位置を復元できます。

 

🧑🏻‍💻 参考


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/


Mac Book の 日本語 JIS キーボードを 英語 US キーボードになるべく違和感なく変える方法

日本語 JIS キーボードが馴染めない場合とか

ストレスすぎますよね。

逆もしかり。

様々なキーボードマッピングカスタマイズアプリなどありますが、

強引にレイアウトを変更したとて、

そもそもキーそれぞれ一つ一つのサイズが違う。

この方法がベストだと思われます。

 

🧑🏻‍💻 Magic Keyboard を手に入れる

少し高いですが。

👉 Magic Keyboard - 英語(US) - Apple(日本)

これを買うなりして手に入れるのが一番操作感は一番近いです。

日本語 JIS と 英語 US。

指紋センサー付きとなし。

Mac Bookのキーボード。

すべてサイズ同じです。

上に乗せればそれが一番良い感じです。

以上です。

知らなかったわ。


【JetpackCompose Navigation3】rememberViewModelStoreNavEntryDecorator() とは何なのか

rememberViewModelStoreNavEntryDecorator()は、Google が開発を進めている次世代のナビゲーションライブラリ Navigation 3 (Android Jetpack) において、特定の画面(NavEntry)に ViewModelStore を提供するためのデコレーターを生成する関数です。


NavDisplay(
    backStack = backStack,
    onBack = { backStack.removeLastOrNull() },
    entryDecorators = listOf(
        rememberSaveableStateHolderNavEntryDecorator(),
        rememberViewModelStoreNavEntryDecorator() // *
    ),

一言でいうと、
「この画面で ViewModel を使えるようにする(ViewModel の器を用意する)」
ための設定項目の一つです。

 

🧑🏻‍💻 役割と仕組み

Navigation 3 では、画面の定義を「デコレーター」という仕組みで拡張します。

  • ViewModel の保持: 通常、ViewModel は ViewModelStore という場所に保存されます。この関数を使うことで、ナビゲーションの各エントリ(画面)が自分自身の ViewModelStore を持てるようになります。
  • ライフサイクルとの連動: これにより、画面が破棄されたときに、その画面に紐づく ViewModel も適切にクリアされるようになります。
  • Shared ViewModel の実現: 親のナビゲーショングラフでこのデコレーターを定義することで、複数の子画面間で同じ ViewModel インスタンスを共有(Shared ViewModel)することも可能になります。


NavDisplay
 └─ NavBackStack
      ├─ NavEntry A
      │    ├─ contentKey = A
      │    └─ ViewModelStore A
      │         └─ ViewModel A
      │
      └─ NavEntry B
           ├─ contentKey = B
           └─ ViewModelStore B
                └─ ViewModel B

 

🧑🏻‍💻 なぜ必要なのか

従来の Navigation Compose では NavHost が内部で自動的に ViewModel の管理を行っていましたが、Navigation 3 はよりシンプルでカスタマイズしやすい設計を目指しています。

そのため、「どの画面が ViewModel の器(Store)を持つか」を明示的に指定する必要があり、そのためにこの関数が用意されています。


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 におけるイベントハンドリングを最もシンプルにする考え方の一つです。