ViewModel 總覽 (Android Jetpack 的一部分)。
ViewModel
類別是商業邏輯或畫面層級的狀態容器。這會向 UI 公開狀態,並封裝相關的商業邏輯。其主要優點在於可快取狀態,並透過設定變更保留。因此,在活動之間導覽或執行設定變更 (例如旋轉畫面) 時,UI 不必再次擷取資料。
如要進一步瞭解狀態容器,請參閱狀態容器指南。同樣,如要進一步瞭解 UI 層,請參閱 UI 層指南。
ViewModel 優點
ViewModel 的替代方案為純類別,用於保留您在 UI 中顯示的資料。在活動或 Navigation 到達網頁之間導覽時,可能會發生問題。如未使用儲存執行個體狀態機制來儲存資料,則系統會將其刪除。ViewModel 提供便捷的 API 來保留資料,以解決此問題。
ViewModel 類別的優點主要有兩點:
- 可讓您保留 UI 狀態。
- 提供商業邏輯存取。
資料持續性
ViewModel 可以透過 ViewModel 保留的狀態,以及 ViewModel 觸發的作業提供資料持續性。使用這項快取功能,您不必在發生常見的設定變更 (例如畫面旋轉) 時再次擷取資料。
範圍
將 ViewModel 執行個體化時,必須傳遞一個實作 ViewModelStoreOwner
介面的物件。這可以是 Navigation 到達網頁、Navigation 圖表、活動、片段,或實作介面的任何其他類型。ViewModel 的範圍則會限定於 ViewModelStoreOwner
的 生命週期內。因此會保留在記憶體中,直到其 ViewModelStoreOwner
永久消失。
各種類別包括 ViewModelStoreOwner
介面的直接或間接子類別。直接子類別為 ComponentActivity
、Fragment
和 NavBackStackEntry
。如需間接子類別的完整清單,請參閱 ViewModelStoreOwner
參考資料。
當 ViewModel 限定範圍所在的片段或活動遭到刪除時,非同步工作會繼續在限定範圍的 ViewModel 中繼續執行。這是要保留的金鑰。
詳情請參閱下方「ViewModel 生命週期」一節。
SavedStateHandle
SaveStateHandle 不僅可在設定變更期間保留資料,在程序重建期間亦是如此。因此,即使使用者關閉應用程式,稍後再次開啟,您仍然可以保持 UI 狀態。
商業邏輯存取
雖然絕大部分商業邏輯都保留在資料層,但 UI 層也可以包含商業邏輯。例如,合併多個存放區的資料來建立畫面 UI 狀態,或者特定類型的資料不需要資料層。
ViewModel 是處理 UI 層中商業邏輯的適當位置。ViewModel 還負責處理事件,並在需要套用商業邏輯以修改應用程式資料時,將事件委派給階層的其他層。
Jetpack Compose
使用 Jetpack Compose 時,ViewModel 是向可組合項公開畫面 UI 狀態的主要方式。在混合式應用程式中,活動和片段只會代管可組合函式。這與過去的方法不同,過去使用活動和片段建立可重複使用的 UI 片段並沒有這麼簡單和直觀,因此,目前的方法可讓做為 UI 控制器的 UI 片段更為有效。
請務必留意,將 ViewModel 與 Compose 搭配使用時,不可將 ViewModel 的範圍限定為可組合項。這是因為可組合項並非 ViewModelStoreOwner
。在 Composition 中,具有相同可組合項的兩個執行個體,或者在同一 ViewModelStoreOwner
下存取相同 ViewModel 類型的不同可組合項,都會收到 ViewModel 的相同執行個體,這通常不是預期行為。
為了在 Compose 中獲得 ViewModel 的優點,請在片段或活動中代管每個畫面;或者使用 Compose Navigation,並且在盡可能靠近 Navigation 到達網頁的可組合函式中使用 ViewModel。這是因為您可以將 ViewModel 的範圍限定為 Navigation 到達網頁、Navigation 圖表、活動及片段。
詳情請參閱 Jetpack Compose 的狀態提升指南。
實作 ViewModel
以下是使用者擲骰子畫面的 ViewModel 實作範例。
Kotlin
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
// Expose screen UI state
private val _uiState = MutableStateFlow(DiceUiState())
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Handle business logic
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
Java
public class DiceUiState {
private final Integer firstDieValue;
private final Integer secondDieValue;
private final int numberOfRolls;
// ...
}
public class DiceRollViewModel extends ViewModel {
private final MutableLiveData<DiceUiState> uiState =
new MutableLiveData(new DiceUiState(null, null, 0));
public LiveData<DiceUiState> getUiState() {
return uiState;
}
public void rollDice() {
Random random = new Random();
uiState.setValue(
new DiceUiState(
random.nextInt(7) + 1,
random.nextInt(7) + 1,
uiState.getValue().getNumberOfRolls() + 1
)
);
}
}
接著,您可以從下列活動中存取清單:
Kotlin
import androidx.activity.viewModels
class DiceRollActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same DiceRollViewModel instance created by the first activity.
// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
val viewModel: DiceRollViewModel by viewModels()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
Java
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same MyViewModel instance created by the first activity.
DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
model.getUiState().observe(this, uiState -> {
// update UI
});
}
}
Jetpack Compose
import androidx.lifecycle.viewmodel.compose.viewModel
// Use the 'viewModel()' function from the lifecycle-viewmodel-compose artifact
@Composable
fun DiceRollScreen(
viewModel: DiceRollViewModel = viewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Update UI elements
}
將協同程式與 ViewModel 搭配使用
ViewModel
包括支援 Kotlin 協同程式。能夠以與保留 UI 狀態相同的方式保留非同步工作。
詳情請參閱「Kotlin 協同程式與 Android 架構元件搭配使用」。
ViewModel 的生命週期
ViewModel
的生命週期與範圍具有直接關聯。ViewModel
會保留在記憶體中,直到限定範圍的 ViewModelStoreOwner
消失為止。以下是可能的發生情境:
- 若是活動,則會在完成時。
- 若是片段,則會在卸離時。
- 若是 Navigation 項目,則從返回堆疊中移除時。
因此,ViewModels 非常適合用於儲存在設定變更後仍然有效的資料。
圖 1 說明活動在進行旋轉然後完成時的不同生命週期狀態。上圖也在關聯的活動生命週期旁顯示 ViewModel
的生命週期。這張特殊圖表說明活動的狀態。相同的基本狀態適用於片段的生命週期。
系統第一次呼叫活動物件的 onCreate()
方法時,您通常會要求 ViewModel
。在活動的整個生命週期中 (例如裝置畫面旋轉時),系統可能會多次呼叫 onCreate()
。ViewModel
存在的時間是從您首次要求 ViewModel
時起算,直到活動完成並刪除為止。
清除 ViewModel 依附元件
當 ViewModelStoreOwner
在生命週期內刪除 ViewModel 時,ViewModel 會呼叫 onCleared
方法。如此一來,您就能清除以 ViewModel 生命週期為依據的任何工作或依附元件。
以下範例顯示 viewModelScope
的替代方案。viewModelScope
是內建的 CoroutineScope
,會自動遵循 ViewModel 生命週期。ViewModel 會根據這個範圍觸發業務相關作業。如果您想使用自訂範圍 (而非 viewModelScope
) 簡化測試程序,ViewModel 可以接收 CoroutineScope
做為建構函式中的依附元件。一旦 ViewModelStoreOwner
在生命週期結束時清除 ViewModel,ViewModel 也會取消 CoroutineScope
。
class MyViewModel(
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {
// Other ViewModel logic ...
override fun onCleared() {
coroutineScope.cancel()
}
}
在 Lifecycle 2.5 以上版本中,您可以傳遞一或多個 Closeable
物件到 ViewModel 的建構函式,該函式會在 ViewModel 執行個體清除時自動關閉。
class CloseableCoroutineScope(
context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
class MyViewModel(
private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
// Other ViewModel logic ...
}
最佳做法
以下是實作 ViewModel 時,應遵循的幾項主要最佳做法:
- 由於範圍限定,請使用 ViewModel 做為畫面層級狀態容器的實作詳細資料。請不要做為可重複使用 UI 元件 (例如方塊群組或表單) 的狀態容器使用。否則會在相同的 ViewModelStoreOwner 下,同一個 UI 元件的不同用法中,取得相同的 ViewModel 執行個體。
- ViewModel 不應瞭解 UI 實作詳細資料。請盡可能保留 ViewModel API 公開的方法名稱,以及 UI 狀態欄位的名稱。這樣一來,ViewModel 就能支援任何類型的 UI:手機、折疊式裝置、平板電腦,甚至是 Chromebook!
- 由於 ViewModel 可能保留的時間比
ViewModelStoreOwner
長,因此 ViewModel 不應保留生命週期相關 API 的任何參照 (例如Context
或Resources
),以免發生記憶體流失。 - 不要將 ViewModel 傳遞給其他類別、函式或其他 UI 元件。由於平台會管理這些元件,因此請盡可能保持靠近。靠近活動、片段或畫面層級可組合函式。這樣可以防止較低層級的元件,存取不必要的資料和邏輯。
其他資訊
隨著資料越趨複雜,您可能會選擇使用獨立的類別來載入資料。ViewModel
的用途是封裝使用者介面控制器的資料,讓資料於設定變更後仍然有效。如要瞭解如何在設定變更時載入、保留及管理資料,請參閱「儲存 UI 狀態」。
「Android 應用程式架構指南」建議建立存放區類別,以處理這些功能。
其他資源
如要進一步瞭解 ViewModel
類別,請參閱下列資源。
說明文件
範例
為您推薦
- 注意:系統會在 JavaScript 關閉時顯示連結文字
- 搭配生命週期感知元件使用 Kotlin 協同程式
- 儲存 UI 狀態
- 載入並顯示分頁資料