在 UI 外部使用 Compose 狀態

Jetpack Compose State<T> 和 MutableState<T> 是由 Compose 快照系統支援的一般用途值容器。雖然最常用於驅動 UI 重組,但快照系統本身與 UI 轉譯無關。

透過 AppState 程式庫 (Transform 和 StateStore),您可以在 ViewModel、管理工具、存放區和背景服務中保留、衍生、觀察及保留 Compose State。

為什麼要在 UI 以外使用 Compose 狀態?

在狀態觀測區塊 (例如 UI 可組合函式、transform 或 derivedStateOf) 中讀取 Compose State 時,Compose 執行階段會在架構的任何層級提供兩項主要優點:

  • 自動和動態依附元件追蹤:讀取狀態的 .value (或使用 by 屬性委派) 會自動將該狀態註冊為區塊的依附元件。由於追蹤是動態的,因此依附元件會在執行階段由執行的程式碼路徑決定。如果未採用條件式分支 (例如在 if 檢查或早期 return 之後),系統就不會讀取該分支中的狀態,且對這些狀態所做的變更不會觸發不必要的重新計算。
  • 一致性和精細程度:您可以宣告獨立、精細的 MutableState 屬性,並將多屬性寫入作業包裝在 Snapshot.withMutableSnapshot 中。中繼更新會隔離至可變動快照,並在區塊完成時自動套用,因此在快照中讀取的觀察器不會看到不一致的中繼狀態,且只有在讀取的特定屬性變更時才會重新計算。與 StateFlow 不同,您不必在單一單體式 StateFlow<UiState> (當任何欄位變更時,會使每個收集器失效) 和獨立的 StateFlow 屬性 (無法在單一原子交易中一起更新) 之間做出選擇。

為架構選擇合適的狀態 API

請根據狀態應保留的時間長度,以及需要存取狀態的元件,選擇保留及保存 Compose State 的方式:

州類別 生命週期和範圍 範例 建議的 Compose State API
啟用狀態 一律儲存在記憶體中;在啟動時查詢最新資料,且不會在程序終止後保留。 網路連線狀態、藍牙連線狀態、暫時性 UI 狀態 (例如項目選取)。
暫時性 UI 狀態 在設定變更和系統啟動的程序終止後仍持續,不會無限期儲存;範圍限於單一畫面或活動。 展開或收合的 UI 元素、捲動位置、畫面搜尋查詢或篩選器切換鈕。
暫時工作階段狀態 在背景程序終止後仍持續,不會無限期儲存;在活動、服務或小工具之間共用。 進行中的專注計時器、多步驟結帳購物車,或填寫中的表單草稿。
永久狀態 永久保留在磁碟上。 使用者偏好設定 (DataStore)、結構化應用程式資料和媒體庫 (Room)。 使用 collectAsState 在 transform 內收集的 DataStore 或 Room 流程

在 ViewModel 中使用 Compose 狀態

在 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 會自動追蹤讀取。請避免使用裸 SavedStateHandle.saved,因為這會傳回一般 Kotlin 屬性,而非 Compose State,且在變更時不會觸發重組。
  • 使用 Snapshot.withMutableSnapshot 進行不可分割的多屬性寫入:在 Snapshot.withMutableSnapshot 內更新 searchQuery 和 selectedTaskIds 會隔離寫入作業,並在單一不可分割的快照交易中提交兩項變更,因此觀察器 (包括 transform) 會在一次重組作業中同時看到兩項更新,而不是觀察到新的 searchQuery 與過時的 selectedTaskIds 配對。
  • 使用 remember(key) 跨重組重複使用流程:使用 searchQuery 做為索引鍵,將 tasksRepository.getTasks(searchQuery) 包裝在 remember 中,可確保當 selectedTaskIds 變更並重組 transform 區塊時,Compose 會重複使用現有的資料庫 Flow,而不是重新啟動查詢。

在應用程式中分享即時系統狀態

對於在多個 ViewModel、活動或服務之間共用的即時系統狀態 (例如網路或藍牙連線狀態),您不希望在程序終止時將過時狀態保留到磁碟。請改為在啟動時查詢目前的系統狀態,將其保留在應用程式範圍單例模式中的私人 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 中,以便自動套用。

應用程式中的任何元件都可以讀取 networkMonitor.isOnline.value,無論是一次性檢查、在 UI 可組合項中,還是在狀態觀察區塊中,例如 listener、derivedStateOf 或 transform。例如,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 會追蹤 networkMonitor.isOnline.value 以及 searchQuery、selectedTaskIds 和存放區 Flow,因此連線能力的任何變更都會自動重新計算 TaskListState.Success,而不會重新查詢資料庫。