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


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 設計の肝は
「型自体にメタ情報を持たせること」
です。

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

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


5-Minute TLS/SSL Troubleshooting Playbook - IP-direct access only (curl / openssl)

 

🧑🏻‍💻 Introduction

When you go through DNS, you can be misled by:

  • caching
  • load balancers / CDNs
  • name-resolution mistakes

This guide standardizes all commands to IP-direct access + correct SNI so you can isolate the real cause quickly.

 

🧑🏻‍💻 Prerequisite Variables


DOMAIN=example.com 
IP=1.2.3.4

 

🧑🏻‍💻 Overall Flow


① Check reachability with curl (IP direct) 
    ↓ 
② Read certificate verification result 
    ↓ 
③ Get raw TLS data with openssl 
    ↓ 
④ Check certificate expiration 
    ↓ 
⑤ Verify SAN 
    ↓ 
⑥ Check intermediate certificate 
    ↓ 
⑦ Verify TLS versions

 

🧑🏻‍💻 ① HTTP Reachability (IP direct + SNI)


curl -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null

OK


* Connected to example.com (1.2.3.4) port 443
* SSL certificate verify ok.
< HTTP/1.1 200 OK

Failure


Connection refused

  • nginx / apache not running
  • closed port
  • firewall

 

🧑🏻‍💻 ③ Raw TLS Layer Information


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN

OK


CONNECTED(00000003)
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)

 

🧑🏻‍💻 ④ Certificate Expiration


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN 2>/dev/null \
 | openssl x509 -noout -dates


notAfter=May 2 23:59:59 2026 GMT

 

🧑🏻‍💻 ⑤ SAN (Domain Match)


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN \
 | openssl x509 -noout -ext subjectAltName


DNS:example.com
DNS:www.example.com

 

🧑🏻‍💻 ⑥ Missing Intermediate Certificate Check


openssl s_client \
 -connect $IP:443 \
 -servername $DOMAIN \
 -showcerts

OK


Certificate chain
 0 s:CN = example.com
 1 s:C = US, O = Let's Encrypt, CN = R3

Missing


Certificate chain
 0 s:CN = example.com

→ fullchain.pem not configured

 

🧑🏻‍💻 ⑦ TLS Version Restrictions

TLS 1.2


curl --tlsv1.2 -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null

TLS 1.3


curl --tlsv1.3 -v https://$DOMAIN \
 --resolve $DOMAIN:443:$IP \
 -o /dev/null


unsupported protocol

→ ssl_protocols misconfiguration

 

🧑🏻‍💻 ⑧ Detect SNI Misconfiguration (intentionally omit it)


openssl s_client -connect $IP:443


subject=CN = default.example.net

→ default certificate returned
→ virtual host configuration issue

 

🧑🏻‍💻 Copy-Paste 5-Minute Diagnosis Set



DOMAIN=example.com 
IP=1.2.3.4 

curl -v https://$IP \
 -H "Host: $DOMAIN"\
 -o /dev/null 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN -brief 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN 2>/dev/null \
 | openssl x509 -noout -dates 

openssl s_client -connect $IP:443 \
 -servername $DOMAIN \
 | openssl x509 -noout -ext subjectAltName

 

🧑🏻‍💻 Root-Cause Shortcut Map


Cannot connect even with IP direct
 → server or firewall 

Verify error 
 → intermediate certificate 

Expired
 → certificate renewal missed

SAN mismatch
 → wrong certificate selected 

Different cert without SNI
 → virtual host configuration 

Only one of TLS1.2 / 1.3 fails
 → protocol restriction

 

🧑🏻‍💻 Summary

By eliminating DNS and fixing:

  • IP-direct access
  • correct SNI

your TLS troubleshooting speed improves dramatically.

This workflow is ready to copy-paste in real incidents.

👉 openssl-s_client - OpenSSL Documentation
👉 curl - SSL CA Certificates


[Jetpack Compose] Implement "Pull-to-Refresh" with the New PullToRefreshBox

The "Pull-to-Refresh" gesture is a staple in Android app UI.

While we previously relied on Modifier.pullRefresh, Jetpack Compose has introduced PullToRefreshBox in Material 3 as the new standard. It's more intuitive and requires much less boilerplate code.

In this post, we’ll quickly cover everything from basic implementation to customization!

 

🧑🏻‍💻 1. Prerequisites

PullToRefreshBox is available in Material 3 (version 1.3.0 or later).

Make sure to check your build.gradle dependencies:


dependencies {
    implementation("androidx.compose.material3:material3:1.3.0")
}

 

🧑🏻‍💻 2. Basic Implementation Pattern

The best part about PullToRefreshBox is that it encapsulates both the refresh logic and the indicator UI into a single component.


@Composable
fun RefreshableListScreen() {
    var isRefreshing by remember { mutableStateOf(false) }
    val scope = rememberCoroutineScope()
    val items = remember { mutableStateListOf("Initial Item A", "Initial Item B") }

    PullToRefreshBox(
        isRefreshing = isRefreshing,
        onRefresh = {
            scope.launch {
                isRefreshing = true
                // Perform your refresh logic (e.g., API calls)
                delay(2000) 
                items.add(0, "New Item ${items.size + 1}")
                isRefreshing = false
            }
        }
    ) {
        LazyColumn(Modifier.fillMaxSize()) {
            items(items) { item ->
                ListItem(headlineContent = { Text(item) })
            }
        }
    }
}

Key Highlights

  • isRefreshing: A boolean that controls the visibility of the refresh indicator.
  • onRefresh: The callback triggered when the user performs the pull gesture.
  • Content Size: Ensure your scrollable content (like LazyColumn) uses Modifier.fillMaxSize() so the pull gesture is detectable across the entire area.

 

🧑🏻‍💻 3. Practical Usage with ViewModel

In a production environment, it's best practice to let a ViewModel handle the state.


class MyViewModel : ViewModel() {
    var isRefreshing by mutableStateOf(false)
        private set

    fun refreshData() {
        viewModelScope.launch {
            isRefreshing = true
            // Simulate network call
            isRefreshing = false
        }
    }
}

val viewModel: MyViewModel = viewModel()
PullToRefreshBox(
    isRefreshing = viewModel.isRefreshing,
    onRefresh = { viewModel.refreshData() }
) {
    // ... Content
}

 

🧑🏻‍💻 4. Customizing the Design

If you want to change the indicator's color to match your brand, use the indicator parameter.


PullToRefreshBox(
    isRefreshing = isRefreshing,
    onRefresh = { /* ... */ },
    indicator = {
        PullToRefreshDefaults.Indicator(
            state = it,
            isRefreshing = isRefreshing,
            containerColor = Color.DarkGray, // Background color
            color = Color.Cyan              // Progress spinner color
        )
    }
) {
    // ...
}

 

🧑🏻‍💻 Conclusion: Simplified Refresh Logic

With the arrival of PullToRefreshBox, implementing this common UI pattern has never been easier.

  • Use Material 3 1.3.0+.
  • Pass the state (isRefreshing).
  • Handle the logic in onRefresh.

That’s it! You now have a modern, native-feeling refresh experience.