Android Compose基于單向數(shù)據(jù)流構(gòu)建界面的過程
構(gòu)建 Compose 界面
在 Jetpack Compose 中構(gòu)建界面,核心邏輯圍繞“狀態(tài)控制”展開,因為 Compose 的界面本身是不可變的 : 一旦繪制完成,就沒法直接修改。我們能操控的只有界面的狀態(tài),只要狀態(tài)發(fā)生變化,Compose 就會智能地重新創(chuàng)建界面樹中那些已經(jīng)改變的部分。像 TextField 這樣的可組合項,就是典型的“接收狀態(tài)、公開事件”的組件:它接收一個值來顯示文本,同時通過 onValueChange 回調(diào)這種事件,來處理用戶修改文本的操作。比如下面這段常見代碼:
var name by remember { mutableStateOf("")}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label ={ Text ("Name" )}
)由于可組合項天生具備“接收狀態(tài)、公開事件”的特性,單向數(shù)據(jù)流模式和 Jetpack Compose 可以說是完美適配。這篇指南就專門講怎么在 Compose 里用單向數(shù)據(jù)流模式、怎么實現(xiàn)事件和狀態(tài)容器,還有如何結(jié)合 ViewModel 使用。
注意:用 Jetpack Compose 開發(fā)界面,并不會影響應(yīng)用的其他層,比如負(fù)責(zé)數(shù)據(jù)存儲的數(shù)據(jù)層和負(fù)責(zé)業(yè)務(wù)邏輯的業(yè)務(wù)層。如果想了解整個應(yīng)用所有層級的構(gòu)建方法,可以參考官方的應(yīng)用架構(gòu)指南。
單向數(shù)據(jù)流
單向數(shù)據(jù)流(UDF)是一種簡單清晰的設(shè)計模式,核心規(guī)則就兩條:狀態(tài)向下流動,事件向上傳遞。采用這種模式后,負(fù)責(zé)顯示狀態(tài)的可組合項,和負(fù)責(zé)存儲、修改狀態(tài)的應(yīng)用部分就能徹底分離開,各司其職。
什么是狀態(tài)向下流動,事件向上傳遞
- 狀態(tài)向下流動
- 核心定義:狀態(tài)(比如界面要顯示的文本內(nèi)容、按鈕是否可點擊、加載框是否顯示等,通常存儲在 ViewModel 或上層可組合項中),是從上層組件/數(shù)據(jù)層傳遞到下層組件/界面層的,下層組件只能被動接收和使用狀態(tài),不能直接修改上層傳來的狀態(tài)。
- Compose 實際場景:
- 比如 ViewModel 中維護著一個
uiState狀態(tài)(包含登錄狀態(tài)、用戶昵稱等數(shù)據(jù)),這個狀態(tài)會向下傳遞給登錄界面、個人中心界面等可組合項; - 父可組合項定義的
isButtonEnabled狀態(tài),向下傳給子可組合項Button,用于控制按鈕是否可點擊; - 下層組件拿到狀態(tài)后,只能用它來渲染界面,比如根據(jù)
uiState中的昵稱顯示文本,根據(jù)isButtonEnabled決定按鈕的交互狀態(tài)。
- 比如 ViewModel 中維護著一個
- 事件向上流動
- 核心定義:事件(比如用戶點擊按鈕、輸入文本、下拉刷新等用戶操作,或是系統(tǒng)通知等),是從下層組件/界面層傳遞到上層組件/數(shù)據(jù)層的,下層組件不處理事件邏輯,只負(fù)責(zé)將事件“上報”,由上層組件統(tǒng)一處理并更新狀態(tài)。
- Compose 實際場景:
- 用戶點擊登錄按鈕,這個點擊事件不會在
Button組件內(nèi)部處理,而是通過onClick回調(diào)向上傳遞給 ViewModel,由 ViewModel 執(zhí)行登錄請求、校驗賬號密碼等業(yè)務(wù)邏輯; - 用戶在
TextField輸入文本,通過onValueChange回調(diào)將輸入事件向上傳遞,由上層組件(或 ViewModel)更新對應(yīng)的文本狀態(tài); - 事件處理完成后,上層組件會更新狀態(tài),而更新后的狀態(tài)又會順著“向下流動”的規(guī)則,重新傳遞給下層界面,觸發(fā)界面重組。
- 用戶點擊登錄按鈕,這個點擊事件不會在
這就是單向數(shù)據(jù)流界面更新的三步完整循環(huán),邏輯一目了然 :
- 產(chǎn)生事件:要么是界面上的操作生成事件(比如用戶點擊按鈕),并把事件向上傳遞給 ViewModel 處理;要么是應(yīng)用其他層傳來事件(比如提示用戶登錄會話已過期)。
- 更新狀態(tài):事件被處理后,對應(yīng)的業(yè)務(wù)邏輯會修改相關(guān)的狀態(tài)。
- 顯示狀態(tài):存儲狀態(tài)的容器會把新狀態(tài)向下傳遞給界面,界面再根據(jù)新狀態(tài)刷新顯示。
遵循該模式的三大優(yōu)勢
在 Jetpack Compose 中使用單向數(shù)據(jù)流,能解決開發(fā)中的不少麻煩,帶來三個核心好處:
- 可測試性更強:狀態(tài)和界面分離開,我們可以單獨測試狀態(tài)的邏輯是否正確,也能單獨驗證界面顯示是否符合預(yù)期,不用混在一起測試。
- 狀態(tài)封裝更嚴(yán)謹(jǐn):狀態(tài)的更新被限制在一個固定位置,而且可組合項的狀態(tài)只有一個可信來源,不會出現(xiàn)多個地方改狀態(tài)導(dǎo)致的狀態(tài)不一致問題,能大幅減少 Bug。
- 界面顯示更一致:只要用
StateFlow或者LiveData這類可觀察的狀態(tài)容器,任何狀態(tài)更新都會立刻反映在界面上,不會出現(xiàn)“狀態(tài)變了,界面沒更”的情況。
Jetpack Compose 中的單向數(shù)據(jù)流
可組合項的工作核心就是“狀態(tài)”和“事件”。比如 TextField,只有當(dāng)它的 value 參數(shù)更新,并且通過 onValueChange 回調(diào)(本質(zhì)是請求改值的事件)觸發(fā)時,它才會更新顯示內(nèi)容。
在 Compose 里,State 對象是專門用來存值的容器,而且它有個關(guān)鍵特性:狀態(tài)值一旦改變,就會觸發(fā)讀取該值的可組合函數(shù)重組。我們存儲狀態(tài)有兩種常用方式,選哪種取決于需要保留狀態(tài)的時長:
remember { mutableStateOf(value) }rememberSaveable { mutableStateOf(value) }
TextField 的值是 String 類型,這個值的來源很靈活,可以是硬編碼的固定值、ViewModel 中的數(shù)據(jù),也可以是從父級可組合項傳過來的。雖然不一定非要把它存在 State 對象里,但必須注意:當(dāng) onValueChange 事件觸發(fā)時,一定要手動更新這個值。
三個核心要點
- mutableStateOf(value):會創(chuàng)建一個
MutableState對象,這是 Compose 里的可觀察類型。只要它的值變了,系統(tǒng)就會安排所有讀取過這個值的可組合函數(shù)進行重組。 - remember:負(fù)責(zé)把對象存儲在組合中。如果調(diào)用
remember的可組合項從組合中被移除了,那它存儲的這個對象也會被忘記。 - rememberSaveable:比
remember多了個“持久化”能力——它會把狀態(tài)存在Bundle里,就算應(yīng)用遇到屏幕旋轉(zhuǎn)這種配置更改,狀態(tài)也不會丟失。
注意:如果想深入了解 Compose 中的狀態(tài)以及狀態(tài)提升的相關(guān)知識,可以參考專門的“狀態(tài)和 Jetpack Compose”指南。
定義可組合項參數(shù)
給可組合項定義狀態(tài)參數(shù)時,別盲目添加,先想清楚兩個問題:這個可組合項需要有多高的可重用性和靈活性?這些狀態(tài)參數(shù)會對它的性能產(chǎn)生什么影響?
核心原則是:為了方便分離邏輯和重復(fù)使用,每個可組合項只包含最必要的信息。舉個例子,我們要做一個顯示新聞標(biāo)題的可組合項 Header,兩種寫法差別很大:
@Composable
fun Header(title: String, subtitle: String) {
// 只有 title 或 subtitle 變化時,才會重組
}
@Composable
fun Header(news: News) {
// 只要傳入新的 News 實例,不管標(biāo)題副標(biāo)題變沒變化,都會重組
}有時候用獨立的參數(shù)還能提升性能。比如 News 類里除了標(biāo)題和副標(biāo)題,還有作者、時間等很多其他信息,這時用 Header(news) 的寫法就很不劃算——哪怕只有無關(guān)信息變了,生成了新的 News 實例,Header 也會無辜重組。
另外還要注意參數(shù)數(shù)量:如果一個可組合函數(shù)的參數(shù)太多,使用起來會很麻煩。這種情況下,建議把這些參數(shù)整合到一個類里,再傳遞這個類的對象。
Compose 中的事件
應(yīng)用里所有用戶輸入或系統(tǒng)通知,都應(yīng)該被當(dāng)作事件來處理——比如按鈕點擊、文本輸入變化,甚至是計時器觸發(fā)、網(wǎng)絡(luò)請求結(jié)果通知等。這些事件觸發(fā)后,不能由界面層直接修改狀態(tài),而是要交給 ViewModel 處理,再由 ViewModel 去更新界面狀態(tài)。
這里有個重要原則:界面層絕對不能在事件處理邏輯之外修改狀態(tài),不然很容易導(dǎo)致應(yīng)用狀態(tài)混亂,出現(xiàn)各種難以排查的 Bug。
傳遞不可變值的優(yōu)勢
給狀態(tài)和事件處理的 lambda 傳遞參數(shù)時,最好用不可變值。這么做有四個明顯好處:
- 提升可重用性:不可變參數(shù)讓可組合項在不同場景下都能使用。
- 防止界面亂改狀態(tài):確保界面只能通過事件回調(diào)觸發(fā)狀態(tài)更新,不能直接修改狀態(tài)值。
- 避免并發(fā)問題:能保證不會有其他線程偷偷修改這個狀態(tài)。
- 降低代碼復(fù)雜度:不可變值的狀態(tài)變化更可控,代碼邏輯更容易理解和維護。
通用可組合項示例
比如應(yīng)用的頂部導(dǎo)航欄(TopAppBar),通常都要顯示文本,還要有個返回按鈕。我們可以做一個通用的 MyAppTopAppBar 可組合項,專門接收文本內(nèi)容和返回按鈕的點擊事件,這樣在整個應(yīng)用里都能復(fù)用:
@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、狀態(tài)和事件:示例
結(jié)合 ViewModel 和 mutableStateOf,我們就能在應(yīng)用中完整實現(xiàn)單向數(shù)據(jù)流。核心要求有兩個:一是界面狀態(tài)要通過 StateFlow 或 LiveData 這類可觀察的狀態(tài)容器公開;二是 ViewModel 要負(fù)責(zé)處理來自界面或其他層的事件,并根據(jù)事件更新狀態(tài)容器。
登錄屏幕的狀態(tài)與事件建模
我們以登錄屏幕為例,看看具體怎么實現(xiàn)。一個登錄屏幕通常有四種狀態(tài),而且狀態(tài)之間是互斥的,用密封類(密封類能限制狀態(tài)的取值范圍,很適合這種場景)建模最合適:
- 退出登錄狀態(tài):用戶還沒登錄的時候。
- 進行中狀態(tài):應(yīng)用正在發(fā)起網(wǎng)絡(luò)請求,嘗試讓用戶登錄的時候(比如顯示加載轉(zhuǎn)圈)。
- 錯誤狀態(tài):登錄過程中出現(xiàn)問題(比如網(wǎng)絡(luò)錯誤、賬號密碼錯誤)。
- 登錄成功狀態(tài):用戶順利登錄后。
ViewModel 里會維護一個私有狀態(tài) _uiState,再對外提供一個只讀的 uiState 供界面訪問。同時,ViewModel 還會提供 onSignIn() 這樣的方法,專門處理登錄事件。代碼示例如下:
class MyViewModel : ViewModel() {
private val _uiState = mutableStateOf<UiState>(UiState.SignedOut)
val uiState: State<UiState> get() = _uiState
// ...
}其他狀態(tài)容器的用法
除了 mutableStateOf,Compose 還為 LiveData、Flow、Observable 這些常用組件提供了擴展,讓它們能注冊為監(jiān)聽器,把自身的值當(dāng)作 Compose 狀態(tài)來使用。比如用 LiveData 的寫法:
class MyViewModel : ViewModel() {
private val _uiState = MutableLiveData<UiState>(UiState.SignedOut)
val uiState: LiveData<UiState> get() = _uiState
// ...
}
@Composable
fun MyComposable(viewModel: MyViewModel) {
val uiState = viewModel.uiState.observeAsState()
// ...
}到此這篇關(guān)于Android Compose 界面架構(gòu) : 基于單向數(shù)據(jù)流的文章就介紹到這了,更多相關(guān)Android Compose 單向數(shù)據(jù)流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
kotlin anko頁面跳轉(zhuǎn)實現(xiàn)方式,攜帶參數(shù)或flag
這篇文章主要介紹了kotlin anko頁面跳轉(zhuǎn)實現(xiàn)方式,攜帶參數(shù)或flag,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android使用AlarmManager設(shè)置鬧鐘功能
這篇文章主要為大家詳細(xì)介紹了Android使用AlarmManager設(shè)置鬧鐘功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-09-09
完全解析Android多線程中線程池ThreadPool的原理和使用
本篇文章給大家通過原理和實例詳細(xì)講述了Android多線程中線程池ThreadPool的原理和使用,對此有興趣的朋友可以跟著參考學(xué)習(xí)下。2018-04-04
Android OkHttp 結(jié)合php 多圖片上傳實例
本篇文章主要介紹了Android OkHttp 結(jié)合php 多圖片上傳實例,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
Android編程之基于Log演示一個activity生命周期實例詳解
這篇文章主要介紹了Android編程之基于Log演示一個activity生命周期,結(jié)合完整實例形式較為詳細(xì)的分析總結(jié)了Log演示activity生命周期的具體用法及Log的具體使用方法,需要的朋友可以參考下2015-12-12
Android Dialog仿ios9中UIAlertController控件
這篇文章主要為大家詳細(xì)介紹了Android Dialog仿ios9中UIAlertController控件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06
解決android報錯:Intel HAXM is required to run this AVD
這篇文章主要介紹了解決android報錯:Intel HAXM is required to run this AVD,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-11-11

