使用 Compose 建構的使用者介面無法變更,即繪製後就無法更新。您可以控制使用者介面的狀態。當使用者介面狀態變更時,Compose 都會重新建立使用者介面樹狀結構中有變動的部分。可組合性可接受狀態並公開事件,例如 TextField
接受值並公開回呼 onValueChange
,該回呼要求回呼處理常式變更值。
var name by remember { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
由於可組合性接受狀態並公開事件,因此單向資料流模式適用於 Jetpack Compose。本指南著重介紹如何在 Compose 中導入單向資料流程模式、如何實作事件和狀態預留位置,以及如何在 Compose 中使用 ViewModels。
單向資料流
單向資料流 (UDF) 是一種設計模式,其中狀態向下流動,事件向上流動。透過單向資料流,您可以將使用者介面中顯示狀態的可組合性從儲存及變更狀態的應用程式分開。
使用單向資料流的應用程式使用者介面更新迴圈如下所示:
- 事件:部分使用者介面會產生事件並向上傳送,例如將按鈕點擊事件傳送給 ViewModel 處理。事件也可能來自應用程式的其他層,例如用於表示使用者工作階段已過期的事件。
- 更新狀態:事件處理常式可能會變更狀態。
- 顯示狀態:狀態預留位置向下傳遞狀態,然後由使用者介面顯示。

使用 Jetpack Compose 時採用下列模式可提供多個優勢:
- 可測試性:從使用者介面分離狀態,就能更方便地在隔離下進行測試。
- 狀態封裝:由於只能在單一位置更新狀態,而且只有一個可組合的可靠狀態來源,因此不太可能由於狀態不一致而產生錯誤。
- 使用者介面一致性:如果使用
StateFlow
或LiveData
等可觀測的狀態容器,使用者介面可以立即反映所有狀態更新。
Jetpack Compose 的單向資料流
基於狀態及事件的可組合性作業。舉例來說,TextField
只有在更新 value
參數時才會更新,且會顯示 onValueChange
回呼,該事件會要求更新值。Compose 將 State
物件定義為值預留位置,而變更狀態值時,就會觸發重編作業。您可以根據所需值保留時間長度,在 remember { mutableStateOf(value) }
或 rememberSaveable { mutableStateOf(value)
中保持狀態。
TextField
可組合值的類型為 String
,因此可以是任何位置,例如硬式編碼值、從 ViewModel,或從父項可組合性值傳入。您不一定要將其保存在 State
物件中,但必須在呼叫 onValueChange
時更新值。
定義可組合的參數
定義可組合的狀態參數時,請注意下列問題:
- 可組合的重複使用性或靈活性如何?
- 狀態參數對這個可組合的效能有何影響?
為鼓勵分離和重複使用,每個可組合都應該盡可能減少持有的資訊量。舉例來說,在建構可組合以保留新聞報導的標題時,最好只傳送需要顯示的資訊,而不是整篇新聞報導
@Composable
fun Header(title: String, subtitle: String) {
// Recomposes when title or subtitle have changed.
}
@Composable
fun Header(news: News) {
// Recomposes when a new instance of News is passed in.
}
有時候,使用個別參數也會提升效能。舉例來說,如果 News
包含比 title
和 subtitle
更多的資訊,當新的 News
執行個體被送入 Header(news)
時,即使 title
和 subtitle
尚未變更,可組合的結構也會有所改變。
請仔細查看您傳送的參數數量。含有過多參數的函式可降低函式的人體工學,因此在這種情況下,最好將其歸入一個類別。
Compose 中的事件
應用程式的所有輸入內容都應該以事件表示:例如觸控、文字變更,甚至是計時器或其他更新。這些事件變更使用者介面的狀態時,應使用 ViewModel 處理這些事件並更新使用者介面的狀態。
使用者介面層不得變更事件處理常式之外的狀態,因為這可能導致應用程式發生不一致和錯誤。
最好傳遞狀態和事件處理常式 lambda 的不可變更值。此方法有以下優點:
- 提高可重複使用性。
- 確保使用者介面不會直接變更狀態值。
- 避免並行問題,因為這可以確保狀態不會從其他執行緒變更。
- 最好是減少程式碼的複雜度。
舉例來說,接受 String
和 lambda 做為參數的可組合性可以從多種結構定義下呼叫,而且可高度重複使用。假設應用程式的頂端應用程式列一律顯示文字,並有返回按鈕。您可以定義較通用的 MyAppTopAppBar
可組合,接收文字和返回按鈕處理常式做為參數:
@Composable
fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) {
TopAppBar(
title = {
Text(
text = topAppBarText,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxSize()
.wrapContentSize(Alignment.Center)
)
},
navigationIcon = {
IconButton(onClick = onBackPressed) {
Icon(
Icons.Filled.ArrowBack,
contentDescription = localizedString
)
}
},
// ...
)
}
ViewModel、狀態及事件:範例
使用 ViewModel
和 mutableStateOf
時,如果符合下列其中一項條件,您也可以在應用程式中導入單向資料流:
- 系統會透過可觀察的狀態持有者顯示使用者介面的狀態,如
StateFlow
或LiveData
。 ViewModel
會處理來自使用者介面或其他應用程式層的事件,並根據事件更新狀態容器。
舉例來說,在實作的登入畫面中輕觸「登入」按鈕,應用程式應該會顯示進度旋轉圖示和網路呼叫。如果成功登入,應用程式會前往另一個螢幕;如果出現錯誤,應用程式會顯示 Snackbar。以下說明模擬螢幕狀態及事件的方法:
螢幕顯示以下四種狀態:
- 已登出:使用者尚未登入。
- 進行中:應用程式目前正透過執行網路呼叫來登入使用者。
- 錯誤:登入時發生錯誤。
- 已登入:使用者已登入。
您可以模擬這些狀態,做為封閉類別ViewModel 會將狀態顯示為 State
、設定初始狀態,並視需要更新狀態。ViewModel 也會提供 onSignIn()
方法來處理登入事件。
sealed class UiState {
object SignedOut : UiState()
object InProgress : UiState()
object Error : UiState()
object SignIn : UiState()
}
class MyViewModel : ViewModel() {
private val _uiState = mutableStateOf<UiState>(SignedOut)
val uiState: State<UiState>
get() = _uiState
// ...
}
除了 mutableStateOf
API 以外,Compose 也提供 LiveData
、Flow
和 Observable
的擴充功能,可註冊為事件監聽器並將值顯示為狀態。
class MyViewModel : ViewModel() {
private val _uiState = MutableLiveData<UiState>(SignedOut)
val uiState: LiveData<UiState>
get() = _uiState
// ...
}
@Composable
fun MyComposable(viewModel: MyViewModel) {
val uiState = viewModel.uiState.observeAsState()
// ...
}
瞭解詳情
如要進一步瞭解 Jetpack Compose 中的架構,請參閱下列資源: