做自(zì)由與創造的先行者

Android狀态容器和(hé)界面狀态

Android開(kāi)發手冊

單向數據流 (UDF) 可作(zuò)爲爲界面層提供和(hé)管理(lǐ)界面狀态的方式,界面層指南介紹了(le)這(zhè)種方式。

此外(wài),該指南還重點介紹了(le)将 UDF 管理(lǐ)委托給名爲狀态容器的特殊類的好(hǎo)處。您可以通過 ViewModel 或普通類實現(xiàn)狀态容器。本文(wén)檔詳細介紹了(le)狀态容器及其在界面層中的作(zuò)用(yòng)。

學完本文(wén)檔後,您應了(le)解如何在界面層中管理(lǐ)應用(yòng)狀态;這(zhè)就是界面狀态生成流水(shuǐ)線。您應該能(néng)夠了(le)解和(hé)掌握以下(xià)内容:

了(le)解界面層中存在的界面狀态類型。

了(le)解在界面層中對(duì)這(zhè)些(xiē)界面狀态執行的邏輯類型。

知(zhī)道(dào)如何選擇合适的狀态容器實現(xiàn)方式,例如 ViewModel 或簡單類。

界面狀态生成流水(shuǐ)線的元素

界面狀态以及生成該狀态的邏輯定義了(le)界面層。

界面狀态

界面狀态是描述界面的屬性。界面狀态有兩種類型:

屏幕界面狀态是需要在屏幕上(shàng)顯示的内容。例如,NewsUiState 類可以包含呈現(xiàn)界面所需的新聞報(bào)道(dào)和(hé)其他(tā)信息。由于該狀态包含應用(yòng)數據,因此通常會(huì)與層次結構中的其他(tā)層相關聯。

界面元素狀态是指界面元素的固有屬性,這(zhè)些(xiē)屬性會(huì)影響界面元素的呈現(xiàn)方式。界面元素可能(néng)處于顯示或隐藏狀态,并且可能(néng)具有特定的字體、字體大(dà)小(xiǎo)或字體顔色。在 Android View 中,View 會(huì)自(zì)行管理(lǐ)此狀态(因爲它本身是有狀态的),并公開(kāi)用(yòng)于修改或查詢其狀态的方法。例如,TextView 類的 get 和(hé) set 方法用(yòng)于顯示該類的文(wén)本。在 Jetpack Compose 中,狀态在可組合項之外(wài),您甚至可以将狀态從(cóng)可組合項附近提升到(dào)執行調用(yòng)的可組合函數或狀态容器中。例如,Scaffold 可組合項的 ScaffoldState。

邏輯

界面狀态不是靜态屬性,因爲應用(yòng)數據和(hé)用(yòng)戶事(shì)件會(huì)導緻界面狀态随時(shí)間而變化。邏輯決定了(le)變化的具體細節,包括界面狀态的哪些(xiē)部分發生了(le)變化、爲什(shén)麽發生變化以及應該在何時(shí)發生變化。

應用(yòng)中的邏輯可以是業務邏輯或界面邏輯:

業務邏輯決定着應用(yòng)數據的産品要求的實現(xiàn)。例如,在新聞閱讀器應用(yòng)中,當用(yòng)戶點按相應按鈕時(shí),就會(huì)爲報(bào)道(dào)添加書簽。這(zhè)種用(yòng)于将書簽保存到(dào)文(wén)件或數據庫的邏輯通常放(fàng)置在領域或數據層中。狀态容器通常通過調用(yòng)這(zhè)類層公開(kāi)的方法,将此邏輯委托給相應的層。

界面邏輯決定着如何在屏幕上(shàng)顯示界面狀态。例如,在用(yòng)戶選擇了(le)某個類别時(shí)獲取正确的搜索欄提示、滾動至列表中的特定項,或者在用(yòng)戶點擊某按鈕時(shí)便進入特定屏幕的導航邏輯。

Android 生命周期以及界面狀态和(hé)邏輯的類型

界面層包含兩個部分:一部分依賴于界面生命周期,另一部分不依賴于界面生命周期。這(zhè)種分離決定了(le)每個部分可用(yòng)的數據源,因此需要不同類型的界面狀态和(hé)邏輯。

不依賴于界面生命周期:界面層的這(zhè)一部分用(yòng)于處理(lǐ)應用(yòng)的數據生成層(數據層或網域層),由業務邏輯定義。界面中的生命周期、配置更改和(hé) Activity 重新創建可能(néng)會(huì)影響界面狀态生成流水(shuǐ)線是否處于活動狀态,但(dàn)不會(huì)影響生成的數據的有效性。

依賴于界面生命周期:界面層的這(zhè)一部分用(yòng)于處理(lǐ)界面邏輯,受生命周期或配置更改的直接影響。這(zhè)些(xiē)更改會(huì)直接影響從(cóng)中讀取數據的來(lái)源的有效性,因此其狀态隻會(huì)在其生命周期處于活動狀态時(shí)發生變化。例如運行時(shí)權限,以及獲取依賴于配置的資源(例如本地化字符串)。

界面狀态生成流水(shuǐ)線

界面狀态生成流水(shuǐ)線是指爲生成界面狀态而執行的步驟。相關步驟包括應用(yòng)上(shàng)文(wén)定義的各類邏輯,并且完全取決于界面的需求。有些(xiē)界面可能(néng)會(huì)受益于流水(shuǐ)線中不依賴于界面生命周期的部分和(hé)/或依賴于界面生命周期的部分,也(yě)可能(néng)不會(huì)受益于其中任一部分。

也(yě)就是說,界面層流水(shuǐ)線的以下(xià)排列是有效的:

由界面本身生成和(hé)管理(lǐ)的界面狀态。例如,一個簡單且可重複使用(yòng)的基本計(jì)數器:

@Composable

fun Counter() {

// The UI state is managed by the UI itself

var count by remember { mutableStateOf(0) }

Row {

Button(onClick = { ++count }) {

Text(text = "Increment")

}

Button(onClick = { --count }) {

Text(text = "Decrement")

}

}

}

界面邏輯 → 界面。例如,顯示或隐藏允許用(yòng)戶跳轉到(dào)列表頂部的按鈕。

@Composable

fun ContactsList(contacts: List) {

val listState = rememberLazyListState()

val isAtTopOfList by remember {

derivedStateOf {

listState.firstVisibleItemIndex < 3

}

}

// Create the LazyColumn with the lazyListState

...

// Show or hide the button (UI logic) based on the list scroll position

AnimatedVisibility(visible = !isAtTopOfList) {

ScrollToTopButton()

}

}

業務邏輯 → 界面。在屏幕上(shàng)展示當前用(yòng)戶的照片的界面元素。

@Composable

fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {

// Read screen UI state from the business logic state holder

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

// Call on the UserAvatar Composable to display the photo

UserAvatar(picture = uiState.profilePicture)

}

業務邏輯 → 界面邏輯 → 界面。會(huì)針對(duì)給定界面狀态在屏幕上(shàng)滾動以顯示正确信息的界面元素。

@Composable

fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {

// Read screen UI state from the business logic state holder

val uiState by viewModel.uiState.collectAsStateWithLifecycle()

val contacts = uiState.contacts

val deepLinkedContact = uiState.deepLinkedContact

val listState = rememberLazyListState()

// Create the LazyColumn with the lazyListState

...

// Perform UI logic that depends on information from business logic

if (deepLinkedContact != null && contacts.isNotEmpty()) {

LaunchedEffect(listState, deepLinkedContact, contacts) {

val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)

if (deepLinkedContactIndex >= 0) {

// Scroll to deep linked item

listState.animateScrollToItem(deepLinkedContactIndex)

}

}

}

}

如果将這(zhè)兩種邏輯都應用(yòng)于界面狀态生成流水(shuǐ)線,則必須始終先應用(yòng)業務邏輯,然後再應用(yòng)界面邏輯。如果嘗試先應用(yòng)界面邏輯,再應用(yòng)業務邏輯,則意味着業務邏輯依賴于界面邏輯。

狀态容器及其責任

狀态容器的責任是存儲狀态,以便應用(yòng)讀取狀态。 在需要邏輯時(shí),它會(huì)充當中介,并提供對(duì)托管所需邏輯的數據源的訪問權限。這(zhè)樣,狀态容器就會(huì)将邏輯委托給相應的數據源。

這(zhè)會(huì)帶來(lái)以下(xià)好(hǎo)處:

簡單的界面:界面僅綁定了(le)其狀态。

可維護性:可以對(duì)狀态容器中定義的邏輯進行叠代,而無需更改界面本身。

可測試性:界面及其狀态生成邏輯可獨立進行測試。

可讀性:代碼讀者可以清楚地看(kàn)出界面呈現(xiàn)代碼與界面狀态生成代碼之間的差異。

無論大(dà)小(xiǎo)或作(zuò)用(yòng)域如何,每個界面元素都與其對(duì)應的狀态容器具有 1 對(duì) 1 關系。此外(wài),狀态容器必須能(néng)夠接受和(hé)處理(lǐ)任何可能(néng)導緻界面狀态發生變化的用(yòng)戶操作(zuò),并且必須生成随後的狀态變化。

注意:狀态容器并非絕對(duì)必要。簡單的界面可能(néng)會(huì)托管内嵌到(dào)其呈現(xiàn)代碼中的邏輯。

狀态容器的類型

與界面狀态和(hé)邏輯的類型類似,界面層中有兩種類型的狀态容器,它們根據自(zì)身與界面生命周期的關系而定義:

業務邏輯狀态容器。

界面邏輯狀态容器。

以下(xià)幾個部分更詳細地介紹了(le)狀态容器的類型,首先講的就是業務邏輯狀态容器。

注意:如果界面邏輯狀态容器依賴于數據層或網域層中的信息,您應從(cóng)業務邏輯狀态容器向界面邏輯狀态容器傳遞該信息。這(zhè)是因爲與界面邏輯狀态容器相比,業務邏輯狀态容器的存在期更長,原因是後者不依賴于界面生命周期。

業務邏輯及其狀态容器

業務邏輯狀态容器會(huì)處理(lǐ)用(yòng)戶事(shì)件,并将數據從(cóng)數據層或網域層轉換爲屏幕界面狀态。

将 ViewModel 用(yòng)作(zuò)業務邏輯狀态容器

ViewModel 在 Android 開(kāi)發中的優勢使其适用(yòng)于提供對(duì)業務邏輯的訪問權限以及準備要在屏幕上(shàng)呈現(xiàn)的應用(yòng)數據。這(zhè)些(xiē)優勢包括如下(xià)各項:

ViewModel 觸發的操作(zuò)在配置發生變化後仍然有效。

與 Navigation 集成:

當屏幕位于返回堆棧中時(shí),Navigation 會(huì)緩存 ViewModel。這(zhè)對(duì)在返回目标位置時(shí)即時(shí)提供之前加載的數據非常重要。使用(yòng)遵循可組合項屏幕的生命周期的狀态容器時(shí),這(zhè)種情況會(huì)更難處理(lǐ)。

當目标位置從(cóng)返回堆棧彈出後,ViewModel 也(yě)會(huì)被一并清除,以确保自(zì)動清理(lǐ)狀态。這(zhè)不同于監聽可組合項的處理(lǐ),監聽的原因可能(néng)有多種,例如轉到(dào)新屏幕、配置發生變化等。

與其他(tā) Jetpack 庫(如 Hilt)集成。

注意:如果 ViewModel 的優勢不适用(yòng)于您的用(yòng)例,或者您以其他(tā)方式執行操作(zuò),則可以将 ViewModel 的責任轉移到(dào)對(duì)普通狀态容器類中。

界面邏輯及其狀态容器

界面邏輯是對(duì)界面本身提供的數據執行操作(zuò)的邏輯。它可能(néng)依賴于界面元素的狀态或界面數據源(如權限 API 或 Resources)。利用(yòng)界面邏輯的狀态容器通常具有以下(xià)屬性:

生成界面狀态并管理(lǐ)界面元素狀态。

在 Activity 重新創建後不再有效:托管在界面邏輯中的狀态容器通常依賴于界面本身的數據源,并且在很(hěn)多情況下(xià),嘗試在配置發生變化後保留此信息會(huì)導緻内存洩漏。如果狀态容器需要數據在配置發生變化後保持不變,則需要将其委托給更适合在 Activity 重新創建後繼續留存的其他(tā)組件。例如,在 Jetpack Compose 中,使用(yòng) remembered 函數創建的可組合界面元素狀态通常會(huì)委托給 rememberSaveable,以便在 Activity 重新創建後保留狀态。此類函數的示例包括 rememberScaffoldState() 和(hé) rememberLazyListState()。

引用(yòng)了(le)界面範圍的數據源:生命周期 API 和(hé)資源等數據源可以安全地引用(yòng)和(hé)讀取,因爲界面邏輯狀态容器與界面具有相同的生命周期。

可在多個不同的界面中重複使用(yòng):同一界面邏輯狀态容器的不同實例可以在應用(yòng)的不同部分中重複使用(yòng)。例如,用(yòng)于管理(lǐ)條狀标簽組的用(yòng)戶輸入事(shì)件的狀态容器可用(yòng)在過濾條件塊的搜索頁上(shàng),也(yě)可以用(yòng)于表示電子郵件接收者的“收件人”字段。

界面邏輯狀态容器通常使用(yòng)普通類實現(xiàn)。這(zhè)是因爲界面本身負責創建界面邏輯狀态容器,而界面邏輯狀态容器與界面本身具有相同的生命周期。例如,在 Jetpack Compose 中,狀态容器是組合的一部分,并遵循組合的生命周期。

注意:當界面邏輯足夠複雜(zá),可以移出界面時(shí),會(huì)使用(yòng)普通類狀态容器。否則,界面邏輯可以在界面中以内嵌方式實現(xiàn)。

Now in Android 示例會(huì)根據設備的屏幕大(dà)小(xiǎo)來(lái)顯示用(yòng)于導航的底部應用(yòng)欄或導航欄。較小(xiǎo)的屏幕使用(yòng)底部應用(yòng)欄,較大(dà)的屏幕則使用(yòng)導航欄。

由于決定 NiaApp 可組合函數中使用(yòng)的适當導航界面元素的邏輯不依賴于業務邏輯,因此可以通過名稱爲 NiaAppState 的普通類狀态容器來(lái)管理(lǐ):

@Stable

class NiaAppState(

val navController: NavHostController,

val windowSizeClass: WindowSizeClass

) {

// UI logic

val shouldShowBottomBar: Boolean

get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||

windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

// UI logic

val shouldShowNavRail: Boolean

get() = !shouldShowBottomBar

// UI State

val currentDestination: NavDestination?

@Composable get() = navController

.currentBackStackEntryAsState().value?.destination

// UI logic

fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

/* ... */

}

在上(shàng)面的示例中,關于 NiaAppState 的以下(xià)詳細信息值得注意:

在 Activity 重新創建後不再有效:通過使用(yòng)遵循 Compose 命名慣例的可組合函數 rememberNiaAppState 創建 NiaAppState,在組合中 remembered 了(le)該容器。重新創建 Activity 後,之前的實例會(huì)丢失,并會(huì)使用(yòng)傳入的所有依賴項(适用(yòng)于重新創建的 Activity 的新配置)創建一個新實例。這(zhè)些(xiē)依賴項可能(néng)是新的,也(yě)可能(néng)是根據以前的配置恢複的。例如,NiaAppState 構造函數中使用(yòng)了(le) rememberNavController(),後者會(huì)委托給 rememberSaveable 以在重新創建 Activity 的過程中保留狀态。

引用(yòng)了(le)界面範圍的數據源:對(duì) navigationController、Resources 和(hé)其他(tā)類似生命周期範圍的類型的引用(yòng)可以安全地保存在 NiaAppState 中,因爲它們具有相同的生命周期作(zuò)用(yòng)域。

注意:建議(yì)爲可重用(yòng)的界面部分(如搜索欄或條狀标簽組)使用(yòng)普通狀态容器類。在這(zhè)種情況下(xià),您不應使用(yòng) ViewModel,因爲 ViewModel 最适合用(yòng)于管理(lǐ)導航目的地的狀态和(hé)對(duì)業務邏輯的訪問權限。

爲狀态容器選擇 ViewModel 和(hé)普通類

在上(shàng)面幾部分中,選擇 ViewModel 還是普通類狀态容器取決于對(duì)界面狀态應用(yòng)的邏輯以及執行該邏輯的數據源。

注意:大(dà)多數應用(yòng)會(huì)選擇執行内嵌在界面本身中的界面邏輯,而這(zhè)些(xiē)邏輯原本可以放(fàng)在普通類狀态容器中。這(zhè)适用(yòng)于簡單的情況,但(dàn)在其他(tā)情況下(xià),您可以通過将邏輯拉取到(dào)普通類狀态容器中來(lái)提高(gāo)可讀性

狀态容器可組合

狀态容器可以依賴于另一個狀态容器,前提是依賴項的生命周期與狀态容器相同或更短。示例如下(xià):

界面邏輯狀态容器可以依賴于另一個界面邏輯狀态容器。

屏幕級狀态容器可以依賴于界面邏輯狀态容器。

以下(xià)代碼段展示了(le) Compose 的 DrawerState 如何依賴于另一個内部狀态容器,即 SwipeableState;還展示了(le)應用(yòng)的界面邏輯狀态容器可以如何依賴于 DrawerState:

@Stable

class DrawerState(/* ... */) {

internal val swipeableState = SwipeableState(/* ... */)

// ...

}

@Stable

class MyAppState(

private val drawerState: DrawerState,

private val navController: NavHostController

) { /* ... */ }

@Composable

fun rememberMyAppState(

drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),

navController: NavHostController = rememberNavController()

): MyAppState = remember(drawerState, navController) {

MyAppState(drawerState, navController)

}

注意:鑒于屏幕級狀态容器管理(lǐ)部分或整個屏幕的業務邏輯複雜(zá)性,因此讓屏幕級狀态容器依賴于另一個屏幕級狀态容器的做法并不合理(lǐ)。如果您遇到(dào)這(zhè)種情況,請(qǐng)重新考慮相關屏幕和(hé)狀态容器,确定您是否真的需要這(zhè)樣做。

舉例來(lái)說,如果界面邏輯狀态容器依賴于屏幕級狀态容器,那麽依賴項的生命周期就比狀态容器更長。這(zhè)會(huì)降低(dī)生命周期較短的狀态容器的可重用(yòng)性,并使其能(néng)夠訪問超出實際需要的邏輯和(hé)狀态。

如果生命周期較短的狀态容器需要來(lái)自(zì)較高(gāo)層級範圍的狀态容器的某些(xiē)信息,請(qǐng)僅将它需要的信息作(zuò)爲參數傳遞,而不是傳遞狀态容器實例。例如,在以下(xià)代碼段中,界面邏輯狀态容器類僅從(cóng) ViewModel 接收所需信息,而不是将整個 ViewModel 實例作(zuò)爲依賴項傳遞。

網站(zhàn)建設開(kāi)發|APP設計(jì)開(kāi)發|小(xiǎo)程序建設開(kāi)發
下(xià)一篇:Android界面狀态生成
上(shàng)一篇:Android界面事(shì)件