Jetpack?Compose狀態(tài)專篇精講
應(yīng)用中的狀態(tài)是指可以隨時(shí)間變化的任何值。這是一個(gè)非常寬泛的定義,從 Room 數(shù)據(jù)庫到類的變量,全部涵蓋在內(nèi)。
由于Compose是聲明式UI,會(huì)根據(jù)狀態(tài)變化來更新UI,因此狀態(tài)的處理至關(guān)重要。這里的狀態(tài)你可以簡單理解為頁面上展示的數(shù)據(jù),那么狀態(tài)管理就是處理數(shù)據(jù)的讀寫。
1.remember
remember
就是用來保存狀態(tài)的,下面舉一個(gè)小例子。
@Composable fun HelloContent() { Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = "", onValueChange = { }, label = { Text("Name") } ) } }
比如我們在頁面中加了一個(gè)輸入框,如果只是上面代碼中這樣處理,那你會(huì)發(fā)現(xiàn)我們輸入的文字不會(huì)被記錄起來,輸入框中始終都是空的。這是因?yàn)閷傩?code>value被固定成了空字符串。我們使用remember
優(yōu)化一下:
@Composable fun HelloContent() { val inputValue = remember { mutableStateOf("") } Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = inputValue.value, onValueChange = { inputValue.value = it }, label = { Text("Name") } ) } }
通過onValueChange
更新value,mutableStateOf
會(huì)創(chuàng)建可觀察的 MutableState<T>
,value 變更時(shí),系統(tǒng)會(huì)重組讀取 value 的所有Composable
函數(shù),這樣就會(huì)自動(dòng)更新UI。
Jetpack Compose 并不強(qiáng)制要求你使用 MutableState 存儲(chǔ)狀態(tài)。Jetpack Compose 支持其他可觀察類型。在 Jetpack Compose 中讀取其他可觀察類型之前,您必須將其轉(zhuǎn)換為 State,以便 Jetpack Compose 可以在狀態(tài)發(fā)生變化時(shí)自動(dòng)重組界面。
LiveData
中可以使用擴(kuò)展函數(shù)observeAsState()
轉(zhuǎn)換為 State。Flow
中可以使用擴(kuò)展函數(shù)collectAsState()
轉(zhuǎn)換為 State。RxJava
中可以使用擴(kuò)展函數(shù)subscribeAsState()
轉(zhuǎn)換為 State。
2.rememberSaveable
雖然 remember
可幫助您在重組后保持狀態(tài),但不會(huì)幫助您在配置更改后保持狀態(tài)。為此,您必須使用 rememberSaveable
。rememberSaveable
會(huì)自動(dòng)保存可保存在 Bundle 中的任何值。
還是上面的例子,如果我們旋轉(zhuǎn)屏幕,就會(huì)發(fā)現(xiàn)輸入框中的文字會(huì)丟失。此時(shí)就可以使用rememberSaveable
替換remember
來幫助我們恢復(fù)界面狀態(tài)。
由于保存的數(shù)據(jù)都是在 Bundle
中的,因此可保存的數(shù)據(jù)類型是有限制的。比如基礎(chǔ)類型、String、Parcelable,Serializable等。一般來說需要保存的對象加個(gè) @Parcelize
注解就可以解決問題。
如果某種原因?qū)е聼o法使用 @Parcelize
,你可以使用 mapSaver
自定義規(guī)則,定義如何將對象保存和恢復(fù)到 Bundle。
data class City(val name: String, val country: String) val CitySaver = run { val nameKey = "Name" val countryKey = "Country" mapSaver( save = { mapOf(nameKey to it.name, countryKey to it.country) }, restore = { City(it[nameKey] as String, it[countryKey] as String) } ) } @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
如果你覺得定義map的key麻煩,可以使用 listSaver
并將其索引用作鍵。
data class City(val name: String, val country: String) val CitySaver = listSaver<City, Any>( save = { listOf(it.name, it.country) }, restore = { City(it[0] as String, it[1] as String) } ) @Composable fun CityScreen() { var selectedCity = rememberSaveable(stateSaver = CitySaver) { mutableStateOf(City("Madrid", "Spain")) } }
3.狀態(tài)提升
對于上面使用到remember
或rememberSaveState
方法來保存狀態(tài)的Composable
函數(shù),我們稱為有狀態(tài)。有狀態(tài)的好處是調(diào)用方不需要控制狀態(tài),并且不必自行管理狀態(tài)。但是,具有內(nèi)部狀態(tài)的Composable
往往不易重復(fù)使用,也更難測試。
在開發(fā)可重復(fù)使用的Composable
時(shí),您通常想要同時(shí)提供同一Composable
的有狀態(tài)和無狀態(tài)版本。有狀態(tài)版本對于不關(guān)心狀態(tài)的調(diào)用方來說很方便,而無狀態(tài)版本對于需要控制或提升狀態(tài)的調(diào)用方來說是必要的。
Compose 中的狀態(tài)提升是一種將狀態(tài)移至調(diào)用方以使可組合項(xiàng)無狀態(tài)的模式。
舉例說明一下狀態(tài)提升,比如我們實(shí)現(xiàn)一個(gè)Dialog,為了方便使用我們可以將里面顯示的文字,點(diǎn)擊事件邏輯寫到dialog的內(nèi)部封裝起來,雖然使用簡單但不具有通用性。那么為了通用,我們可以將文字,點(diǎn)擊事件的回調(diào)當(dāng)參數(shù)傳入,這樣就靈活了起來。
狀態(tài)提升其實(shí)就是這樣一個(gè)編程思想,只是換了個(gè)名詞,沒有什么特別了。
對于上面輸入框的例子,我們用狀態(tài)提示優(yōu)化一下:
@Composable fun HelloScreen() { var name by rememberSaveable { mutableStateOf("") } HelloContent(name = name, onNameChange = { name = it }) } @Composable fun HelloContent(name: String, onNameChange: (String) -> Unit) { Column(modifier = Modifier.padding(16.dp)) { OutlinedTextField( value = name, onValueChange = onNameChange, label = { Text("Name") } ) } }
這樣就實(shí)現(xiàn)了Composable
函數(shù)HelloContent 與狀態(tài)的存儲(chǔ)方式解耦,便于我們復(fù)用。
狀態(tài)下降、事件上升的這種模式稱為“單向數(shù)據(jù)流”。在這種情況下,狀態(tài)會(huì)從 HelloScreen 下降為 HelloContent,事件會(huì)從 HelloContent 上升為 HelloScreen。通過遵循單向數(shù)據(jù)流,您可以將在界面中顯示狀態(tài)的可組合項(xiàng)與應(yīng)用中存儲(chǔ)和更改狀態(tài)的部分解耦。
4.狀態(tài)管理
根據(jù)可組合項(xiàng)的復(fù)雜性,需要考慮不同的備選方案:
將Composable作為可信來源
用于管理簡單的界面元素狀態(tài)。比如上一篇提到的LazyColumn
滾動(dòng)到指定item,將交互都放在當(dāng)前的Composable
中進(jìn)行。
val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() LazyColumn( state = listState, ) { /* ... */ } Button( onClick = { coroutineScope.launch { listState.animateScrollToItem(index = 0) } } ) { ... }
其實(shí)查看rememberLazyListState
的源碼,可以看到實(shí)現(xiàn)很簡單:
@Composable fun rememberLazyListState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0 ): LazyListState { return rememberSaveable(saver = LazyListState.Saver) { LazyListState( initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset ) } }
將狀態(tài)容器作為可信來源
當(dāng)可組合項(xiàng)包含涉及多個(gè)界面元素狀態(tài)的復(fù)雜界面邏輯時(shí),應(yīng)將相應(yīng)事務(wù)委派給狀態(tài)容器。這樣做更易于單獨(dú)對該邏輯進(jìn)行測試,還降低了可組合項(xiàng)的復(fù)雜性。該方法支持分離關(guān)注點(diǎn)原則:可組合項(xiàng)負(fù)責(zé)發(fā)出界面元素,而狀態(tài)容器包含界面邏輯和界面元素的狀態(tài)。
@Composable fun MyApp() { MyTheme { val myAppState = rememberMyAppState() Scaffold( scaffoldState = myAppState.scaffoldState, bottomBar = { if (myAppState.shouldShowBottomBar) { BottomBar( tabs = myAppState.bottomBarTabs, navigateToRoute = { myAppState.navigateToBottomBarRoute(it) } ) } } ) { NavHost(navController = myAppState.navController, "initial") { /* ... */ } } } }
rememberMyAppState
代碼:
class MyAppState( val scaffoldState: ScaffoldState, val navController: NavHostController, private val resources: Resources, /* ... */ ) { val bottomBarTabs = /* State */ val shouldShowBottomBar: Boolean get() = /* ... */ fun navigateToBottomBarRoute(route: String) { /* ... */ } fun showSnackbar(message: String) { /* ... */ } } @Composable fun rememberMyAppState( scaffoldState: ScaffoldState = rememberScaffoldState(), navController: NavHostController = rememberNavController(), resources: Resources = LocalContext.current.resources, /* ... */ ) = remember(scaffoldState, navController, resources, /* ... */) { MyAppState(scaffoldState, navController, resources, /* ... */) }
其實(shí)就是再封裝一層,用戶處理邏輯。封裝的部分就叫狀態(tài)容器,用于管理Composable的邏輯和狀態(tài)。
將 ViewModel 作為可信來源
一種特殊的狀態(tài)容器類型,用于提供對業(yè)務(wù)邏輯以及屏幕或界面狀態(tài)的訪問權(quán)限。
ViewModel 的生命周期比Composable長,因此不應(yīng)保留對綁定到組合生命周期的狀態(tài)的長期引用。否則,可能會(huì)導(dǎo)致內(nèi)存泄漏。建議屏幕級Composable使用 ViewModel 來提供對業(yè)務(wù)邏輯的訪問權(quán)限并作為其界面狀態(tài)的可信來源。如需了解 ViewModel 為何適用于這種情況,請參閱 ViewModel 和狀態(tài)容器部分。
本篇到此結(jié)束,幫忙點(diǎn)個(gè)贊~ 給我一點(diǎn)鼓勵(lì),你也可以收藏本篇以備不時(shí)之需。
參考
到此這篇關(guān)于Jetpack Compose狀態(tài)專篇精講的文章就介紹到這了,更多相關(guān)Jetpack Compose狀態(tài)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android仿網(wǎng)易新聞圖片詳情下滑隱藏效果示例代碼
這篇文章主要給大家介紹了關(guān)于利用Android如何仿網(wǎng)易新聞圖片詳情下滑隱藏效果的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解
這篇文章主要介紹了Android TextWatcher內(nèi)容監(jiān)聽死循環(huán)案例詳解,本篇文章通過簡要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08Android自定義View實(shí)現(xiàn)可拖拽縮放的矩形框
這篇文章主要為大家詳細(xì)介紹了Android自定義View實(shí)現(xiàn)可拖拽縮放的矩形框,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-05-05Android View事件分發(fā)和消費(fèi)源碼簡單理解
這篇文章主要介紹了Android View事件分發(fā)和消費(fèi)源碼簡單理解的相關(guān)資料,需要的朋友可以參考下2017-07-07Android仿微信進(jìn)度彈出框的實(shí)現(xiàn)方法
最近公司項(xiàng)目需要實(shí)現(xiàn)類似微信進(jìn)度條彈出框效果,其實(shí)現(xiàn)方法并不難,下面給大家介紹下Android仿微信進(jìn)度彈出框的實(shí)現(xiàn)方法,需要的朋友參考下吧2017-01-01Android學(xué)習(xí)筆記(二)之電話撥號(hào)器
目前手機(jī)市場上android已經(jīng)具有強(qiáng)大的霸主地位,吸引了很多的追棒者,android學(xué)習(xí)越來越火熱,本文給大家介紹android學(xué)習(xí)筆記(二)之電話撥號(hào)器,感興趣的朋友一起學(xué)習(xí)吧2015-11-11Android捕捉錯(cuò)誤try catch 的簡單使用教程
這篇文章主要介紹了Android捕捉錯(cuò)誤try catch 的簡單使用,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09Android無限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案
這篇文章主要介紹了Android無限循環(huán)RecyclerView的完美實(shí)現(xiàn)方案,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06