Jetpack Compose の State<T> と MutableState<T> は、Compose スナップショット システムを基盤とする汎用の値ホルダーです。通常、スナップショットは UI の再コンポーズを促進するために使用されますが、スナップショット システム自体は UI レンダリングに関連付けられていません。
AppState ライブラリ(Transform と StateStore)を使用すると、ViewModel、マネージャー、リポジトリ、バックグラウンド サービス全体で Compose State を保持、派生、監視、永続化できます。
UI 以外で Compose State を使用する理由
状態を監視するブロック(UI コンポーザブル、transform、derivedStateOf など)内で Compose State を読み取ると、Compose ランタイムはアーキテクチャのどのレイヤでも次の 2 つの主なメリットを提供します。
- 自動的かつ動的な依存関係のトラッキング: 状態の
.valueを読み取る(またはbyプロパティ委任を使用する)と、その状態がブロックの依存関係として自動的に登録されます。トラッキングは動的であるため、依存関係は実行時にどのコードパスが実行されるかによって決まります。条件分岐が実行されない場合(ifチェックや早期returnの後など)、その分岐内の状態は読み取られず、状態の変更によって不要な再計算がトリガーされることはありません。 - 整合性と粒度: 個別のきめ細かい
MutableStateプロパティを宣言し、複数のプロパティの書き込みをSnapshot.withMutableSnapshotでラップできます。中間更新は、変更可能なスナップショットに分離され、ブロックの完了時にアトミックに適用されます。そのため、スナップショット内で読み取りを行うオブザーバーは、一貫性のない中間状態を認識することはなく、読み取った特定のプロパティが変更された場合にのみ再計算を行います。StateFlowとは異なり、単一のモノリシックなStateFlow<UiState>(いずれかのフィールドが変更されると、すべてのコレクタが無効になる)と、個別のStateFlowプロパティ(単一のアトミック トランザクションで一緒に更新できない)のどちらかを選択する必要はありません。
アーキテクチャ全体で適切な状態 API を選択する
Compose の State を保持して永続化する方法は、状態を保持する期間と、その状態にアクセスする必要があるコンポーネントに基づいて選択します。
| 州のカテゴリ | 存続期間とスコープ | 例 | 推奨される Compose State API |
|---|---|---|---|
| ライブ状態 | 常にメモリに保存されます。起動時に新しくクエリされ、プロセスの終了をまたいで保持されることはありません。 | ネットワーク接続ステータス、Bluetooth 接続ステータス、一時的な UI 状態(アイテムの選択など)。 |
|
| 一時的な UI 状態 | 構成変更やシステムによるプロセスの終了後も保持されますが、無期限に保存されるわけではありません。単一の画面または Activity にスコープされます。 | 展開または折りたたまれた UI 要素、スクロール位置、画面検索クエリ、フィルタの切り替え。 |
|
| 一時的なセッション状態 | バックグラウンド プロセスの終了後も保持されるが、無期限に保存されるわけではない。アクティビティ、サービス、ウィジェット間で共有される。 | アクティブなフォーカス タイマー、複数ステップの購入手続きカート、進行中のフォームの下書き。 |
|
| 永続的な状態 | ディスクに無期限に保持されます。 | ユーザー設定(DataStore)、構造化されたアプリデータとメディア ライブラリ(Room)。 | collectAsState を使用して transform 内で収集された DataStore または Room のフロー |
ViewModel で Compose の State を使用する
ViewModel 内では、独立した画面入力をきめ細かい MutableState プロパティとして宣言し、SavedStateHandle.saveable を使用してプロセスの終了後も保持し、Snapshot.withMutableSnapshot を使用して複数のプロパティをアトミックに更新し、transform 内でリポジトリの Flow ストリームと組み合わせることができます。
sealed interface TaskListState { data object Loading : TaskListState data class Success( val tasks: List<Task>, val selectedTaskIds: Set<String>, val isOnline: Boolean = true, ) : TaskListState } @HiltViewModel class TaskListViewModel @Inject constructor( private val tasksRepository: TasksRepository, savedStateHandle: SavedStateHandle, ) : ViewModel() { var selectedTaskIds by mutableStateOf(emptySet<String>()) private set var searchQuery by savedStateHandle.saveable { mutableStateOf("") } private set fun updateSearchQuery(query: String) { Snapshot.withMutableSnapshot { searchQuery = query selectedTaskIds = emptySet() } } val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, ) } }
この ViewModel の主なパターン:
- リアクティブな保存済み状態に
SavedStateHandle.saveableを使用する:mutableStateOf(androidx.lifecycle:lifecycle-viewmodel-composeから)を使用してsearchQueryをSavedStateHandle.saveableに委任すると、プロセスの終了をまたいで値がMutableStateとして保持されるため、transformは読み取りを自動的に追跡します。Compose のStateではなく、プレーンな Kotlin プロパティを返し、変更時に再コンポジションをトリガーしない、裸のSavedStateHandle.savedは避けてください。 Snapshot.withMutableSnapshotを使用したアトミックなマルチプロパティ書き込み:Snapshot.withMutableSnapshot内でsearchQueryとselectedTaskIdsを更新すると、書き込みが分離され、両方の変更が単一のアトミックなスナップショット トランザクションで commit されます。そのため、オブザーバー(transformを含む)は、古いselectedTaskIdsとペアになった新しいsearchQueryを監視するのではなく、1 回の再コンポーズ パスで両方の更新をまとめて確認します。remember(key)を使用して再コンポーズ間でフローを再利用する:searchQueryをキーとしてrememberでtasksRepository.getTasks(searchQuery)をラップすると、selectedTaskIdsが変更されてtransformブロックが再コンポーズされるときに、Compose はクエリを再開するのではなく、既存のデータベースFlowを再利用します。
アプリ全体でライブ システムの状態を共有する
複数の ViewModel、アクティビティ、サービスで共有されるライブシステムの状態(ネットワークや Bluetooth の接続ステータスなど)については、プロセスの終了をまたいで古いステータスをディスクに保持する必要はありません。代わりに、起動時に現在のシステム状態をクエリし、アプリケーション スコープのシングルトン内のプライベート mutableStateOf プロパティに保持して、読み取り専用の State<T> を公開します。
interface NetworkMonitor { val isOnline: State<Boolean> } @Singleton class LiveNetworkMonitor @Inject constructor( @ApplicationContext context: Context, ) : NetworkMonitor { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) private val _isOnline = mutableStateOf(checkCurrentConnectivity()) override val isOnline: State<Boolean> = _isOnline private val callback = object : ConnectivityManager.NetworkCallback() { override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities, ) { _isOnline.value = networkCapabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } override fun onLost(network: Network) { _isOnline.value = false } } init { connectivityManager.registerDefaultNetworkCallback(callback) } private fun checkCurrentConnectivity(): Boolean { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability( NetworkCapabilities.NET_CAPABILITY_INTERNET ) } }
MutableState は、任意のスレッドからの整合性のある読み取りと更新をサポートしているため、NetworkCallback はバックグラウンド コールバック スレッドから _isOnline.value を直接更新できます。バックグラウンド コールバックが複数の MutableState プロパティをまとめて更新する場合は、書き込みを Snapshot.withMutableSnapshot でラップして、アトミックに適用されるようにします。
アプリ内の任意のコンポーネントは、1 回限りのチェックとして、UI コンポーザブル内で、または listener、derivedStateOf、transform などの状態監視ブロック内で、networkMonitor.isOnline.value を読み取ることができます。たとえば、TaskListViewModel は transform ブロック内で networkMonitor.isOnline.value を読み取ることができます。
val tasksState: State<TaskListState> = transform<TaskListState>( initialValue = TaskListState.Loading, scope = viewModelScope, ) { val tasks = remember(searchQuery) { tasksRepository.getTasks(searchQuery) }.collectAsState(initial = null).value ?: return@transform TaskListState.Loading TaskListState.Success( tasks = tasks, selectedTaskIds = selectedTaskIds, isOnline = networkMonitor.isOnline.value, ) }
transform は searchQuery、selectedTaskIds、リポジトリ Flow とともに networkMonitor.isOnline.value を追跡するため、接続性が変更されると、データベースに再クエリすることなく TaskListState.Success が自動的に再計算されます。