系列第五篇厨内,進入 Compose 中有關(guān) State 狀態(tài)的學習。
前面幾篇筆記講了那么多內(nèi)容渺贤,都是基于靜態(tài)界面的展示來說的雏胃,即給我一個不變的數(shù)據(jù),然后將它展示出來志鞍。如何在 Compose 中構(gòu)建一個隨數(shù)據(jù)而變化的動態(tài)界面呢瞭亮?相信看完這篇就知道了。
1固棚、基本知識
眾所周知统翩,Compose 徹底舍棄了 xml 文件,我們需要像 Flutter 一樣完全用代碼去進行界面的編碼此洲,這樣做很容易會導致一個問題:界面和數(shù)據(jù)處理邏輯耦合厂汗,從而導致 Activity 中代碼臃腫且維護性下降。
雖然提出了許多架構(gòu)思想呜师,如 MVC娶桦、MVP、MVVM 等汁汗,一定程度上解耦了界面與數(shù)據(jù)處理邏輯趟紊,但是架構(gòu)本身就具有一定的復(fù)雜性,且對于后續(xù)維護成本也相對較高碰酝,所以 Compose 一開始就將界面與數(shù)據(jù)分開來霎匈,分別稱之為 組合 和 State 狀態(tài)秀仲。
State 狀態(tài):官方文檔上說 State 狀態(tài)是指可以隨時間變化的任何值夸盟。例如,它可能是存儲在 Room 數(shù)據(jù)庫中的值戚炫、類的變量,加速度計的當前讀數(shù)等墨吓。
怎么理解這個概念呢球匕?我覺得可以簡單理解為:我們要展示給用戶看的數(shù)據(jù)。例如帖烘,一個商品的展示頁面亮曹,其實就是根據(jù)數(shù)據(jù)的不同來展示不同的狀態(tài),數(shù)據(jù)正常秘症、數(shù)據(jù)錯誤照卦、空數(shù)據(jù)等不同的數(shù)據(jù)就是代表了不同的 State 狀態(tài)。
組合:按照文檔上的意思我覺得可以理解為展示給用戶的界面乡摹,是由多個組合項(Composable組件)組成役耕。
Event事件:指的是從應(yīng)用外部生成的輸入,用于通知程序的某部分發(fā)生了變化聪廉。如用戶的點擊瞬痘,滑動等操作。所以在 Compose 中板熊,Event 事件一般就是引起 State 狀態(tài)改變的原因框全。
2、狀態(tài)的表示
其實可以換一種說法:Compose 中數(shù)據(jù)的存儲和更新如何處理干签?目前來看的話津辩,可以用 LiveData、StateFlow筒严、Flow、Observable 等表示情萤⊙纪埽可以看出,這些都是一種可觀察數(shù)據(jù)變化的容器筋岛,被它們修飾的對象娶视,我們都可以觀察到該對象的變化,從而更新界面睁宰。沒錯肪获,都是使用的觀察者模式。
在 Compose 的文檔中柒傻,ViewModel 被推薦為 State狀態(tài)的管理對象孝赫,從而實現(xiàn)將數(shù)據(jù)與界面展示的 Activity 分離解耦的目的。
2.1 ViewModel
ViewModel 也是 Jetpack 工具庫的成員之一红符,主要用來存儲 UI 展示所需要的數(shù)據(jù)青柄,谷歌推薦的做法是將 Activity 中的數(shù)據(jù)都放到 ViewModel 里伐债,而且在 Activity、Fragment 重建時 ViewModel 中的數(shù)據(jù)是不受影響的致开。還可以通過 ViewModel 來進行 Activity 與 Fragment 之間峰锁,或者 Fragment 與 Fragment 之間的通信。
ViewModel 經(jīng)常與 LiveData 一起使用双戳,但在 Compose 中虹蒋,推薦使用 MutableState 來具體存儲數(shù)據(jù)的值。
2.2 MutableState<T>
MutableState<T> 是 Compose 中內(nèi)置的專門用于存儲 State 狀態(tài)的容器飒货,與 LiveData 一樣魄衅,它可以觀察到存儲的值的變化。如果項目不是純 Compose 代碼膏斤,建議還是用 LiveData徐绑,因為 LiveData 是通用的,而 MutableState<T> 是與 Compose 集成了莫辨,所以在 Compose 中使用 MutableState 比 LiveData 更簡單傲茄。
從這里也可看出,Compose 是推薦將 State 狀態(tài)設(shè)置為可觀察的沮榜,這樣當狀態(tài)發(fā)生更改時盘榨,Compose 可以自動重組更新界面。
實際上 MutableState<T> 是個接口:
// code 1
interface MutableState<T>: State<T> {
override var value: T
}
對 value 進行的任何更改都會自動重組用于讀取此狀態(tài)的所有 Composable 函數(shù)蟆融,也就是說草巡,value 值改變了之后,所有引用了 value 的 Composable 函數(shù)都會重新繪制更新型酥。
3山憨、一個簡單例子
先來看看效果:
其中有兩個控件,一個是 Text弥喉,用于顯示輸入的內(nèi)容郁竟;另一個是 TextField,相當于 View 體系中的 EditText由境∨锬叮可以看出,Text 顯示的內(nèi)容可以隨著下面的 TextField 中輸入的內(nèi)容實時更新虏杰。
如果是在 View 體系中讥蟆,一般實現(xiàn)的方法是在 EditText 添加一個 TextWatcher 類用于監(jiān)聽輸入事件,然后在 onTextChanged 方法中對 TextView 設(shè)置輸入的內(nèi)容即可纺阔。
再來看一下 Compose 是如何實現(xiàn)這一小功能的 瘸彤。根據(jù)官方推薦,得先有一個 ViewModel 進行狀態(tài)數(shù)據(jù)的管理:
// code 2
class ZhuViewModel: ViewModel() {
// 狀態(tài)數(shù)據(jù)初始化笛钝,初始化為空字符串
var inputStr = mutableStateOf("")
// 狀態(tài)更新方法钧栖,將新輸入的內(nèi)容賦值給 MutableState<T> 對象的 value 值
fun onInputChange(inputContent: String) {
inputStr.value = inputContent
}
}
可以看出低零,ViewModel 中需要對狀態(tài)進行初始化,并且提供相應(yīng)的更新方法拯杠。同時 ViewModel 中不會出現(xiàn)任何與界面相關(guān)的對象掏婶,例如 Activity、Fragment潭陪、Context 等雄妥,為的就是解耦。
界面代碼就是 Composable 函數(shù)根據(jù) ViewModel 管理的 State 狀態(tài)進行展示:
// code 3
class ZhuStateActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val zhuViewModel by viewModels<ZhuViewModel>()
setContent {InputShow(zhuViewModel)}
}
}
@Composable
fun InputShow(viewModel: ZhuViewModel) {
Column(Modifier.padding(20.dp)) {
Text(text = viewModel.inputStr.value)
TextField(
value = viewModel.inputStr.value,
onValueChange = { viewModel.onInputChange(it) }
)
}
}
TextField 組件相當于 EditText依溯,onValueChange 可獲取到用戶的輸入內(nèi)容老厌,在這里調(diào)用 ViewModel 中更新狀態(tài)的方法。這樣黎炉,所有引用了 ViewModel 中 MutableState 類型對象 inputStr 的組合項(Composable 函數(shù))枝秤,都會自動重繪更新,Text 組件就可以實時更新輸入的內(nèi)容了慷嗜。
4. remember 關(guān)鍵字
其實在 code 3 中的小功能使用 ViewModel 來管理 State 狀態(tài)有點小題大做了淀弹,可以用 remember 關(guān)鍵字來實現(xiàn)。這個關(guān)鍵字的作用如它的意思一樣庆械,“記住” 它所修飾的對象的值薇溃。下面的代碼就是沒有使用 ViewModel 的實現(xiàn)方法:
// code 4
class ZhuStateActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {InputShow()}
}
}
@SuppressLint("UnrememberedMutableState")
@Composable
fun InputShow() {
val inputStr = mutableStateOf("Hello")
Column(Modifier.padding(20.dp)) {
Text(text = inputStr.value)
TextField(
value = inputStr.value,
onValueChange = {
inputStr.value = it
}
)
}
}
這里沒有使用 remember 會有紅線提醒,我先使用 SuppressLint 去掉了報錯缭乘,為的只是舉個栗子沐序,并且設(shè)置了默認展示 “Hello” 文案。運行一下堕绩,你會發(fā)現(xiàn)策幼,不管輸入什么,都只是展示 “Hello”奴紧,好像啥也沒有發(fā)生特姐。。绰寞。
這是為啥到逊?加一些 log 看看:
// code 5
@SuppressLint("UnrememberedMutableState")
@Composable
fun InputShow() {
val inputStr = mutableStateOf("Hello")
Log.d(TAG, "InputShow: Column inputStr = ${inputStr.value}")
Column(Modifier.padding(20.dp)) {
Text(text = inputStr.value)
TextField(
value = inputStr.value,
onValueChange = {
inputStr.value = it
Log.d(TAG, "InputShow: onValueChange inputStr = $it")
}
)
}
}
連續(xù)輸入字母 w铣口、o滤钱、r、l脑题、d件缸,打出來的 log 是這樣的:
可見在每次輸入之后,都會觸發(fā) Composable 函數(shù)重新繪制叔遂,每次都會重新初始化 inputStr 這個狀態(tài)他炊,而初始值都是一樣的争剿,所以看起來就好像輸入不起作用。Composable 函數(shù)的重新繪制過程也被稱之為 重組痊末。
重組:使用新的輸入Event事件重新調(diào)用可組合項以更新 Compose 樹的過程蚕苇。這一過程會再次運行相同的 Composable 組件進行更新。
順帶說一下凿叠,Compose 首次運行渲染 Composable 組件時涩笤,會為所有被調(diào)用的 Composable 組件構(gòu)建一個樹,然后在重組期間會使用新的 Composable 組件去更新樹盒件。
再回到這個例子蹬碧,使用 remember 關(guān)鍵字就可以避免每次重組時都初始化為初始值。使用后的代碼為:
// code 6
@Composable
fun InputShow() {
val inputStr = remember{ mutableStateOf("Hello") }
Column(Modifier.padding(20.dp)) {
Text(text = inputStr.value)
TextField(
value = inputStr.value,
onValueChange = {
inputStr.value = it
}
)
}
}
這樣就可以正確實現(xiàn)功能了炒刁。其實 remember 關(guān)鍵字的使用是由兩部分組成:
- key arguments:表示這次調(diào)用使用的 “鍵”(key)恩沽,用圓括號包裹;
- calculation :一個 Lambda 表達式翔始,計算得出需要存儲的 “值”(value)罗心。
所以,remember 的用法如下所示:
// code 7
remember(key) { calculation: () -> T }
remember 關(guān)鍵字可以為 Composable 組件項提供一個數(shù)據(jù)存儲空間绽昏,系統(tǒng)會將由 calculation Lambda 表達式計算得出的值存儲到組合樹中协屡,只有當 remember 的 “鍵” 發(fā)生變化時,才會重新執(zhí)行 calculation 計算得出 value 并存儲起來全谤;否則還是原來的值肤晓。
當然 code 6 中并沒有設(shè)置 remember 的 key,這種情況下认然,remember 會默認該 key 沒有發(fā)生變化补憾,不會重新初始化,而是用之前的值卷员。
需要注意的點: remember 雖然會將數(shù)據(jù)或?qū)ο蟠鎯υ诮M合項中盈匾,但當調(diào)用 remember 的可組合項從組合樹中移除后,它會忘記該數(shù)據(jù)或?qū)ο蟊下狻K韵鞫灰谟刑砑踊蛞瞥?Composable 組件的情況下,使用 remember 將重要內(nèi)容存儲在 Composable 組件中未巫,因為添加和移除都會使得數(shù)據(jù)丟失窿撬。
5. 狀態(tài)提升
狀態(tài)提升的概念是對于 Composable 組件來說的,根據(jù) Composable 組件中是否含有 State 狀態(tài)可分為 有狀態(tài)可組合項 和 無狀態(tài)可組合項叙凡。 如 code 6 中的 InputShow 組合項就是一個有狀態(tài)可組合項劈伴。
5.1 有狀態(tài)與無狀態(tài)
Flutter 中的 Widget 也是分為 StatefulWidget 和 StatelessWidget,想不到 Compose 也借用了這個設(shè)計思想握爷。
有狀態(tài)可組合項是一種具有可隨時間變化狀態(tài)的 Composable 組件跛璧。再說具體一點严里,就是 Composable 組件里有類似于 remember 存儲的狀態(tài),而且該組件會在內(nèi)部保持和改變自己的狀態(tài)追城。調(diào)用方不需要控制狀態(tài)刹碾。缺點是,具有內(nèi)部狀態(tài)的可組合項復(fù)用性往往不高座柱,也更難以測試教硫。
無狀態(tài)可組合項就是指無法直接更改任何狀態(tài)的 Composable 組件。因為不包含任何狀態(tài)數(shù)據(jù)辆布,所以它更容易測試瞬矩,復(fù)用性也更高。
如果需要將有狀態(tài)組合項轉(zhuǎn)變?yōu)闊o狀態(tài)組合項锋玲,則需要 狀態(tài)提升景用。
5.2 狀態(tài)提升怎么做?
Compose 中的狀態(tài)提升是一種將狀態(tài)移至可組合項的調(diào)用方以使可組合項無狀態(tài)的模式惭蹂。常規(guī)的狀態(tài)提升模式是將狀態(tài)變量替換為兩個參數(shù):
-
value: T
:要顯示的當前值伞插; -
onValueChange: (T) -> Unit
:請求更改值的事件,其中的 T 是新值
這種方式提升的狀態(tài)具有一些重要的屬性:
- 單一可信來源: 狀態(tài)提升并不是將狀態(tài)復(fù)制盾碗,而是將狀態(tài)移動到上層的可組合項中媚污,這樣可確保只有一個可信來源,減少數(shù)據(jù)不一致所導致的 bug廷雅;
- 封裝: 只有有狀態(tài)可組合項可以修改其狀態(tài)耗美,可以理解為是內(nèi)部“自治”的;
- 可共享: 提升后的狀態(tài)可以與多個可組合項共享航缀;
- 可攔截: 無狀態(tài)可組合項的調(diào)用方可以在更改狀態(tài)之前決定忽略或者修改事件商架;
- 解耦: 無狀態(tài)可組合項的狀態(tài)可以存儲在任何位置,如 ViewModel 中芥玉。
具體怎么做可以看下面的一個小栗子蛇摸。
5.3 狀態(tài)提升小栗子
根據(jù)上述所說,很容易就可以得知 code 6 的 InputShow Composable 組件是一個有狀態(tài)的可組合項灿巧,它包含一個狀態(tài)變量 inputStr赶袄,所以,我們要將這個 MutableState 用兩個參數(shù)進行替換抠藕,一個是要顯示的當前值饿肺;另一個是 Lambda 表達式,用于請求更改值的事件幢痘,就可以將其改寫為一個無狀態(tài)可組合項唬格。如下 code 8 所示:
// code 8 無狀態(tài)可組合項 InputShow
@Composable
fun InputShow(inputText: String, onInputChange: (String)-> Unit) {
Column(Modifier.padding(20.dp)) {
Text(text = inputText)
TextField(
value = inputText,
onValueChange = onInputChange
)
}
}
那狀態(tài)提升到哪里去了呢家破?通常會提升到它的父組件中颜说,那么父組件就是一個有狀態(tài)的可組合項了购岗,這個例子中 InputShow 的父組件這里定義為 InputShowContainer:
// code 9
@Composable
fun InputShowContainer() {
val (inputStr, setInput) = remember{ mutableStateOf("") }
InputShow(inputStr, setInput)
}
嗯?MutableState 的聲明與之前的不太一樣了门粪,多出來的這個 setInput 也是一個 Lambda 表達式喊积,用于更新值。其實玄妈,聲明 MutableState 對象的方法總共有三種:
val mutableState = remember{ mutableStateOf(default) }
val value by remember{ mutableStateOf(default) }
val (value, setValue) = remember{ mutableStateOf(default) }
所以這里用的是第三種聲明方法乾吻。這樣,InputShow 組合項就經(jīng)過狀態(tài)提升變?yōu)榱藷o狀態(tài)的可組合項了拟蜻。官方在這里還特意說明绎签,在 Composable 組件中創(chuàng)建 State<T>(或其他有狀態(tài)對象)時,務(wù)必對其執(zhí)行 remember 操作酝锅,否則它會在每次重組時重新初始化诡必。
6. 狀態(tài)存儲的其他方式
由前述所說,remember 關(guān)鍵字可存儲組合項中的狀態(tài)搔扁,但是一旦組合項被移動爸舒,這些狀態(tài)就丟失了,那如果涉及到橫豎屏切換等 Activity 重建的應(yīng)用場景稿蹲,該怎么辦呢扭勉?雖然保存在 ViewModel 中可以解決問題,但總有點小題大做了苛聘。下面是狀態(tài)存儲的一些其他的方式涂炎。
6.1 rememberSaveable
這個與 remember 類似,主要用于 Activity 或進程重建時设哗,恢復(fù)界面狀態(tài)璧尸。還是上面 code 6 的栗子,可以試試橫豎屏切換或其他配置項更改熬拒,會發(fā)現(xiàn)使用 remember 關(guān)鍵字時爷光,切換后就回到初始空白值了。改為 rememberSaveable 后切換后輸入的內(nèi)容可以保存下來而不會被重置澎粟。
這么看的話蛀序,rememberSaveable 有點像是 override fun onSaveInstanceState(outState: Bundle)
方法了,確實是這樣的活烙,任何可以存儲在 Bundle 對象中的數(shù)據(jù)都可以通過 rememberSaveable 進行保存徐裸。無法用 Bundle 進行保存的數(shù)據(jù),可以用下面的方式進行存儲啸盏。
6.2 Parcelize
最簡單的解決方法就是在對象上添加 @Parcelize 注解重贺,對象就可以轉(zhuǎn)化為可打包狀態(tài)且可以捆綁。還記得 Java 中的 Serializable 接口嗎?是一樣的作用气笙,都是將實例對象編碼成字節(jié)流進行存儲次企。
在日常 Android 開發(fā)中如果不涉及到本地化存儲或者網(wǎng)絡(luò)傳輸?shù)那闆r,推薦使用 Parcelable潜圃,因為相比于 Serializable 它不會產(chǎn)生大量臨時對象缸棵,沒有使用反射,效率更高谭期。但很多時候不想寫 Parcelable 接口的模板代碼堵第,那么就可以使用這個注解!下面是樣例及使用步驟:
// code 10
// app/build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-parcelize' // 第一步:添加此插件
}
@Parcelize // 第二步:添加注解及 Parcelable 接口
data class City(val name: String, val country: String) : Parcelable
// 這樣就可以將其保存到狀態(tài)中
val cityBean = rememberSaveable{ mutableStateOf(City("0112","西京"))}
終于隧出,Parcelable 和 Serializable 接口一樣好用了踏志!
6.3 MapSaver
Compose 還考慮到有些情況下 Parcelize 不適用的場景,那么還可以使用 MapSaver 來定義自己的存儲和恢復(fù)規(guī)則胀瞪,規(guī)定如何把對象轉(zhuǎn)為可保存到 Bundle 中的值狰贯。
// code 11
data class Book(val name: String, val author: String)
val BookSaver = run {
val nameKey = "Name"
val authorKey = "Author"
mapSaver(
save = { mapOf(nameKey to it.name, authorKey to it.author) },
restore = { Book(it[nameKey] as String, it[authorKey] as String) }
)
}
val chosenBook = rememberSaveable( stateSaver = BookSaver ) {
mutableStateOf(Book("三體","劉慈欣"))
}
核心在 BookSaver 這個 Saver 對象,通過 save 這個 lambda 可以將 Book 對象轉(zhuǎn)化為一個 Map 進行存儲赏廓;要使用的時候就通過 restore 這個 lambda 將 Map 又恢復(fù)為一個 Book 對象涵紊。
6.4 ListSaver
MapSaver 需要自己去定義 Key 值,但使用 ListSaver 就可以不用自己定義 Key幔摸,本質(zhì)上是把對象放在一個 List 中存儲摸柄,所以它是使用索引作為 Key。
// code 12
val BookListSaver = listSaver<Book, Any>(
save = { listOf(it.name, it.author) },
restore = { Book(it[0] as String, it[1] as String) }
)
使用起來與 MapSaver 一樣既忆,只不過存儲的數(shù)據(jù)結(jié)構(gòu)不一樣罷了驱负。實際上,MapSaver 底層也是用 ListSaver 實現(xiàn)的患雇。
總結(jié)
最后來個總結(jié)吧跃脊。
- Compose 為了實現(xiàn)解耦將界面和數(shù)據(jù)分離開來,分別稱之為 組合 與 State 狀態(tài)苛吱。為了達到狀態(tài)改變自動重組界面的目的酪术,引入了 MutableState<T> 來存儲 State 狀態(tài)的容器。
- MutableState<T> 的 value 一旦改變翠储,所有引用它的 Composable 組件都會重組绘雁,從而保證了數(shù)據(jù)與顯示的一致性。此外援所,為了保證每次重組時 State 狀態(tài)不會被初始化為初值庐舟,Compose 引入 remember 關(guān)鍵字來將數(shù)據(jù)存儲在相應(yīng)的 Composable 組件中。
- remember 關(guān)鍵字是根據(jù)傳入的鍵是否改變來返回相應(yīng)的值住拭。鍵改變了則返回初值挪略;鍵未變則返回上次存儲的值历帚。不設(shè)置鍵,則默認鍵始終不變杠娱,即始終取上次的值挽牢。
- 為了解決 remember 關(guān)鍵字不能在 Activity 重建等場景下保存數(shù)據(jù)而引入了 rememberSaveable、MapSaver墨辛、ListSaver 等狀態(tài)保存及恢復(fù)的方法。
- Compose 把 Composable 組件分為有狀態(tài)與無狀態(tài)兩類趴俘,內(nèi)部含有 State 狀態(tài)的就為有狀態(tài)可組合項睹簇;反之則為無狀態(tài)組合項。無狀態(tài)組合項復(fù)用性更高寥闪,而有狀態(tài)組合項可以自己管理State狀態(tài)太惠。通過狀態(tài)提升可以將有狀態(tài)組合項轉(zhuǎn)化為無狀態(tài)組合項。
- Compose 推薦使用 ViewModel 來管理狀態(tài)疲憋,包括狀態(tài)的更新以及存儲等凿渊。
參考文獻
- 官方文檔——在Jetpack Compose 中使用狀態(tài) https://developer.android.google.cn/codelabs/jetpack-compose-state?
- Compose 狀態(tài)與組合 新小夢 https://juejin.cn/post/6937560914254102565
- 【背上Jetpack之ViewModel】即使您不使用MVVM也要了解ViewModel ——ViewModel 的職能邊界 Flywith24 https://juejin.cn/post/6844904100493017095
- Jetpack Compose學習之mutableStateOf與remember是什么 柚子君下 https://blog.csdn.net/weixin_43662090/article/details/116120540
- 官方文檔——狀態(tài)和 Jetpack Compose https://developer.android.google.cn/jetpack/compose/state
更多內(nèi)容,歡迎關(guān)注我的同名公眾號留言交流~