架構(gòu)層
每一層均基于較低的層逐級構(gòu)建,并通過組合功能來創(chuàng)建更高級別的組件。每一層都是基于較低層的公共 API 構(gòu)建的,用于驗證模塊邊界,還支持根據(jù)需要替換任何層。
Material: 此模塊為 Compose 界面提供了 Material Design 系統(tǒng)的實(shí)現(xiàn),同時提供了一個主題系統(tǒng)以及若干樣式化組件、漣漪效果指示元素和圖標(biāo)。在應(yīng)用中使用 Material Design 時,不妨基于此層進(jìn)行構(gòu)建。
基礎(chǔ):?此模塊為 Compose 界面提供了與設(shè)計系統(tǒng)無關(guān)的構(gòu)建塊,例如?Row?和?Column、LazyColumn、特定手勢的識別等。可以考慮基于基礎(chǔ)層構(gòu)建自己的設(shè)計系統(tǒng)。
界面: 界面層由多個模塊(ui-text、ui-graphics?和?ui-tooling?等)組成。這些模塊實(shí)現(xiàn)了界面工具包的基本組件,例如?LayoutNode、Modifier、輸入處理程序、自定義布局和繪圖。如果只需要用到界面工具包的基本概念,則可以考慮基于此層進(jìn)行構(gòu)建。
運(yùn)行時此模塊提供了 Compose 運(yùn)行時的基本組件,例如?remember、mutableStateOf、@Composable?注釋和?SideEffect。如果只需要 Compose 的樹管理功能,而不需要其界面,則可以考慮直接基于此層進(jìn)行構(gòu)建。
Jetpack Compose 的一個指導(dǎo)原則是提供可以組合在一起的重點(diǎn)突出的小塊功能片段,而不是幾個單體式組件。這種方法有許多優(yōu)點(diǎn)。
控制:更高級別的組件往往能完成更多操作,但擁有的直接控制權(quán)較少。如果需要更多控制權(quán),可以使用較低級別的組件。使用較低級別的 API 的過程更為復(fù)雜,但可提供更多的控制權(quán)。
自定義:通過將較小的構(gòu)建塊組合成更高級別的組件,可大幅降低按需自定義組件的難度。如果希望在組件的參數(shù)之外進(jìn)行自定義,則可以“降級”并復(fù)刻某個組件。
選擇合適的抽象化級別:Compose 以構(gòu)建可重復(fù)使用的分層組件作為理念,這意味著不應(yīng)該始終以構(gòu)建較低級別的構(gòu)建塊為目標(biāo)。許多較高級別的組件不僅能夠提供更多功能,而且通常還會融入最佳實(shí)踐,例如支持無障礙功能等。一般來講,最好基于能提供所需功能的最高級別的組件進(jìn)行構(gòu)建,以便從其包含的最佳實(shí)踐中受益。
在 Compose 中,界面是不可變的,在繪制后無法進(jìn)行更新。可以控制的是界面的狀態(tài)。每當(dāng)界面的狀態(tài)發(fā)生變化時,Compose 都會重新創(chuàng)建界面樹中已更改的部分??山M合項可以接受狀態(tài)并公開事件,例如?TextField?接受值并公開請求回調(diào)處理程序更改值的回調(diào)?onValueChange。
由于可組合項接受狀態(tài)并公開事件,因此單向數(shù)據(jù)流模式非常適合 Jetpack Compose。
采用 Jetpack Compose 不會影響應(yīng)用的其他層(數(shù)據(jù)層和業(yè)務(wù)層)。
單向數(shù)據(jù)流
單向數(shù)據(jù)流 (UDF) 是一種設(shè)計模式,在該模式下狀態(tài)向下流動,事件向上流動。通過采用單向數(shù)據(jù)流,可以將在界面中顯示狀態(tài)的可組合項與應(yīng)用中存儲和更改狀態(tài)的部分分離開來。
使用單向數(shù)據(jù)流的應(yīng)用的界面更新循環(huán)如下所示:
事件:界面的某一部分生成一個事件,并將其向上傳遞,例如將按鈕點(diǎn)擊傳遞給 ViewModel 進(jìn)行處理;或者從應(yīng)用的其他層傳遞事件,如指示用戶會話已過期。
更新狀態(tài):事件處理腳本可能會更改狀態(tài)。
顯示狀態(tài):狀態(tài)容器向下傳遞狀態(tài),界面顯示此狀態(tài)。
使用 Jetpack Compose 時遵循此模式可帶來下面幾項優(yōu)勢:
可測試性:將狀態(tài)與顯示狀態(tài)的界面分離開來,更方便單獨(dú)對二者進(jìn)行測試。
狀態(tài)封裝:因為狀態(tài)只能在一個位置進(jìn)行更新,并且可組合項的狀態(tài)只有一個可信來源,所以不太可能由于狀態(tài)不一致而出現(xiàn) bug。
界面一致性:通過使用可觀察的狀態(tài)容器,例如?StateFlow?或?LiveData,所有狀態(tài)更新都會立即反映在界面中。
Jetpack Compose 中的單向數(shù)據(jù)流
可組合項基于狀態(tài)和事件進(jìn)行工作。例如,只有在更新其?value?參數(shù)并公開?onValueChange?回調(diào)(這是一個請求將值更改為新值的事件)時,TextField?才會更新。Compose 將?State?對象定義為值容器,而對狀態(tài)值的更改會觸發(fā)重組??梢詫顟B(tài)保存在?remember { mutableStateOf(value) }?或?rememberSaveable { mutableStateOf(value)?中,具體取決于需要記住值的時長。
TextField?可組合項的值的類型為?String,因此該值可以來自任意位置,包括來自硬編碼值、ViewModel 或從父級可組合項傳入。不必將它保存在?State?對象中,但在調(diào)用?onValueChange?時需要更新該值。
mutableStateOf(value)?會創(chuàng)建一個?MutableState,后者是 Compose 中的可觀察類型。如果其值有任何更改,系統(tǒng)會安排重組讀取此值的所有可組合函數(shù)。
remember?會將對象存儲在組合中,當(dāng)調(diào)用?remember?的可組合項從組合中移除后,它會忘記該對象。
rememberSaveable?通過將狀態(tài)保存在?Bundle?中來保留狀態(tài),使其在配置更改后仍保持不變。
可組合項的生命周期
一個組合將描述應(yīng)用的界面,并通過運(yùn)行可組合項來生成。組合是描述界面的可組合項的樹結(jié)構(gòu)。
當(dāng) Jetpack Compose 首次運(yùn)行可組合項時,在初始組合期間,它將跟蹤為了描述組合中的界面而調(diào)用的可組合項。然后,當(dāng)應(yīng)用的狀態(tài)發(fā)生變化時,Jetpack Compose 會安排重組。重組是指 Jetpack Compose 重新執(zhí)行可能因狀態(tài)更改而更改的可組合項,然后更新組合以反映所有更改。
組合只能通過初始組合生成且只能通過重組進(jìn)行更新。重組是修改組合的唯一方式。
可組合項的生命周期通過以下事件定義:進(jìn)入組合,執(zhí)行 0 次或多次重組,然后退出組合。
重組通常由對?State<T>?對象的更改觸發(fā)。Compose 會跟蹤這些操作,并運(yùn)行組合中讀取該特定?State<T>?的所有可組合項以及這些操作調(diào)用的無法跳過的所有可組合項。
如果某一可組合項多次被調(diào)用,在組合中將放置多個實(shí)例。每次調(diào)用在組合中都有自己的生命周期。
組合中可組合項的實(shí)例由其調(diào)用點(diǎn)進(jìn)行標(biāo)識。Compose 編譯器將每個調(diào)用點(diǎn)都視為不同的調(diào)用點(diǎn)。從多個調(diào)用站點(diǎn)調(diào)用可組合項會在組合中創(chuàng)建多個可組合項實(shí)例。
調(diào)用點(diǎn)是調(diào)用可組合項的源代碼位置。這會影響其在組合中的位置,因此會影響界面樹。
在重組期間,可組合項調(diào)用的可組合項與上個組合期間調(diào)用的可組合項不同,Compose 將確定調(diào)用或未調(diào)用的可組合項,對于在兩次組合中均調(diào)用的可組合項,如果其輸入未更改,Compose 將避免重組這些可組合項。
保留身份對于將附帶效應(yīng)與可組合項相關(guān)聯(lián)十分重要,這樣它們才能成功完成,而不是每次重組時都重新啟動。
多次調(diào)用同一可組合項也會多次將其添加到組合中。如果從同一個調(diào)用點(diǎn)多次調(diào)用某個可組合項,Compose 就無法唯一標(biāo)識對該可組合項的每次調(diào)用,因此除了調(diào)用點(diǎn)之外,還會使用執(zhí)行順序來區(qū)分實(shí)例。這種行為有時是必需的,但在某些情況下會導(dǎo)致發(fā)生意外行為。
使用?key?可組合項幫助 Compose 識別組合中的可組合項實(shí)例。當(dāng)從同一個調(diào)用點(diǎn)調(diào)用多個可組合項,且這些可組合項包含附帶效應(yīng)或內(nèi)部狀態(tài)時,這一點(diǎn)非常重要。
在重組期間,如果某些符合條件的可組合函數(shù)的輸入未從先前組合中發(fā)生變化,則可以完全跳過它們的執(zhí)行。
可組合函數(shù)可以跳過,除非:
該函數(shù)具有非?Unit?返回值類型
該函數(shù)帶有?@NonRestartableComposable?或?@NonSkippableComposable?注解。
必需參數(shù)屬于非穩(wěn)定類型
有一個實(shí)驗性編譯器模式: 強(qiáng)跳過,該模式放寬了最后一個要求。
為了讓某個類型被視為穩(wěn)定類型,它必須遵循以下協(xié)定:
對于相同的兩個實(shí)例,其?equals?的結(jié)果將始終相同。
如果類型的某個公共屬性發(fā)生變化,組合將收到通知。
所有公共屬性類型也都是穩(wěn)定。
此協(xié)定中有一些重要的通用類型,即使沒有使用?@Stable?注解將其明確標(biāo)記為穩(wěn)定,Compose 編譯器也會將其視為穩(wěn)定類型:
所有基元值類型:Boolean、Int、Long、Float、Char?等。
字符串
所有函數(shù)類型 (lambda)
所有這些類型都可以遵循穩(wěn)定協(xié)定,因為它們是不可變的。由于不可變類型絕不會發(fā)生變化,它們就永遠(yuǎn)不必通知組合更改方面的信息,因此遵循該協(xié)定就容易得多。
Compose 的?MutableState?類型是一種眾所周知穩(wěn)定但可變的類型。如果?MutableState?中存儲了值,狀態(tài)對象整體會被視為穩(wěn)定對象,因為?State?的?.value?屬性如有任何更改,Compose 就會收到通知。
當(dāng)作為參數(shù)傳遞到可組合項的所有類型都很穩(wěn)定時,系統(tǒng)會根據(jù)可組合項在界面樹中的位置來比較參數(shù)值,以確保相等性。如果所有值自上次調(diào)用后未發(fā)生變化,則會跳過重組。
Compose 僅在可以證明穩(wěn)定的情況下才會認(rèn)為類型是穩(wěn)定的。例如,接口通常被視為不穩(wěn)定類型,并且具有可變公共屬性的類型(實(shí)現(xiàn)可能不可變)的類型也被視為不穩(wěn)定類型。
如果 Compose 無法推斷某個類型的穩(wěn)定性,請為該類型添加?@Stable?注解,讓 Compose 優(yōu)先選擇智能重組。
定義可組合項參數(shù)
在定義可組合項的狀態(tài)參數(shù)時,應(yīng)牢記以下問題:
可組合項的可重用性或靈活性如何?
狀態(tài)參數(shù)如何影響此可組合項的性能?
為了促進(jìn)分離和重復(fù)使用,每個可組合項都應(yīng)包含盡可能少的信息。例如,構(gòu)建可組合項以保存新聞報道的標(biāo)題時,最好僅傳遞需要顯示的信息,而不是整篇新聞報道
有時,使用獨(dú)立參數(shù)還能提高性能,例如,如果?News?包含的不僅僅是?title?和?subtitle?的信息,每當(dāng)有?News?的新實(shí)例傳入?Header(news)?時,即使?title?和?subtitle?沒有變化,可組合項也將重組。
請仔細(xì)考慮傳入的參數(shù)數(shù)量。如果一個函數(shù)擁有過多參數(shù),會降低該函數(shù)的工效,因此在這種情況下,建議將這些參數(shù)分到一個類下。
Compose 中的事件
應(yīng)用的每項輸入都應(yīng)表示為事件:點(diǎn)按、文本更改,甚至計時器或其他更新。當(dāng)這些事件會更改界面的狀態(tài)時,應(yīng)由?ViewModel?來處理它們并更新界面狀態(tài)。
界面層絕不應(yīng)更改事件處理腳本之外的狀態(tài),因為這樣做可能會導(dǎo)致應(yīng)用出現(xiàn)不一致和 bug。
最好為狀態(tài)和事件處理腳本 lambda 傳遞不可變值。此方法具有以下優(yōu)勢:
提升可重用性。
確保界面不會直接更改狀態(tài)的值。
避免并發(fā)問題,確保不會從其他線程修改狀態(tài)。
通常情況下,還可以降低代碼的復(fù)雜性。
例如,接受?String?和 lambda 作為參數(shù)的可組合項可以從許多上下文中調(diào)用,并且可重用性較高。假設(shè)應(yīng)用中的頂部應(yīng)用欄始終顯示文本并包含返回按鈕??梢远x一個更通用的?MyAppTopBar?可組合項,該可組合項用于接收文本和返回按鈕句柄作為參數(shù)
ViewModel、狀態(tài)和事件
借助?ViewModel?和?mutableStateOf,如果出現(xiàn)以下任一情況,還可以在應(yīng)用中引入單向數(shù)據(jù)流:
界面的狀態(tài)通過?StateFlow?或?LiveData?等可觀察的狀態(tài)容器公開。
ViewModel?處理來自應(yīng)用界面或其他層的事件,并根據(jù)事件更新狀態(tài)容器。
例如,在實(shí)現(xiàn)登錄屏幕時,點(diǎn)按登錄按鈕應(yīng)該會使應(yīng)用顯示一個進(jìn)度旋轉(zhuǎn)圖標(biāo)和網(wǎng)絡(luò)調(diào)用。如果登錄成功,應(yīng)用會轉(zhuǎn)到其他屏幕;如果發(fā)生錯誤,應(yīng)用會顯示信息提示控件。以下是如何為屏幕狀態(tài)和事件建模的方法:
該屏幕有四種狀態(tài):
退出登錄:當(dāng)用戶尚未登錄時。
進(jìn)行中:當(dāng)應(yīng)用目前正在嘗試通過執(zhí)行網(wǎng)絡(luò)調(diào)用來讓用戶登錄時。
錯誤:登錄時出現(xiàn)錯誤。
登錄成功:用戶登錄后。
可以將這些狀態(tài)建模為密封類。ViewModel?以?State?的形式公開狀態(tài),設(shè)置初始狀態(tài),并根據(jù)需要更新狀態(tài)。ViewModel?還會通過公開?onSignIn()?方法來處理登錄事件。
除了?mutableStateOf?API 之外,Compose 還提供?LiveData、Flow?和?Observable?的擴(kuò)展,用于注冊為監(jiān)聽器,并將值表示為狀態(tài)。
Jetpack Compose 的階段
與大多數(shù)其他界面工具包一樣,Compose 會通過幾個不同的“階段”來渲染幀。
Compose 有 3 個主要階段:
組合:要顯示什么樣的界面。Compose 運(yùn)行可組合函數(shù)并創(chuàng)建界面說明。
布局:要放置界面的位置。該階段包含兩個步驟:測量和放置。對于布局樹中的每個節(jié)點(diǎn),布局元素都會根據(jù) 2D 坐標(biāo)來測量并放置自己及其所有子元素。
繪制:渲染的方式。界面元素會繪制到畫布(通常是設(shè)備屏幕)中。
這些階段通常會以相同的順序執(zhí)行,讓數(shù)據(jù)能夠沿一個方向(從組合到布局,再到繪制)生成幀(也稱為單向數(shù)據(jù)流)。BoxWithConstraints?以及?LazyColumn?和?LazyRow?是值得注意的特例,其子級的組合取決于父級的布局階段。
可以放心地假設(shè)每個幀都會以虛擬方式經(jīng)歷這 3 個階段,但為了保障性能,Compose 會避免在所有這些階段中重復(fù)執(zhí)行根據(jù)相同輸入計算出相同結(jié)果的工作。如果可以重復(fù)使用前面計算出的結(jié)果,Compose 會跳過對應(yīng)的可組合函數(shù);如果沒有必要,Compose 界面不會對整個樹進(jìn)行重新布局或重新繪制。Compose 只會執(zhí)行更新界面所需的最低限度的工作。之所以能夠?qū)崿F(xiàn)這種優(yōu)化,是因為 Compose 會跟蹤不同階段中的狀態(tài)讀取。
組合:在組合階段,Compose 運(yùn)行時會執(zhí)行可組合函數(shù)并輸出表示界面的樹形結(jié)構(gòu)。此界面樹由多個布局節(jié)點(diǎn)組成,其中包含后續(xù)階段所需的所有信息。
布局:?在布局階段,Compose 使用在組合階段生成的界面樹作為輸入。布局節(jié)點(diǎn)的集合包含確定每個節(jié)點(diǎn)在 2D 空間中的大小和位置所需的所有信息。
在布局階段,系統(tǒng)使用以下三步算法遍歷布局樹:
測量子節(jié)點(diǎn):節(jié)點(diǎn)會測量其子節(jié)點(diǎn)(如果存在)。
確定自己的大小:節(jié)點(diǎn)根據(jù)這些測量結(jié)果決定自己的大小。
放置子節(jié)點(diǎn):每個子節(jié)點(diǎn)均相對于節(jié)點(diǎn)自身的位置進(jìn)行放置。
在此階段結(jié)束時,每個布局節(jié)點(diǎn)都會:
已指定的?width?和?height
應(yīng)繪制它的?x、y 坐標(biāo)
Compose 運(yùn)行時只需遍歷界面樹一次即可測量和放置所有節(jié)點(diǎn),從而提高性能。當(dāng)樹中的節(jié)點(diǎn)數(shù)量增加時,遍歷該樹所花的時間將以線性方式增加。相反,如果多次訪問每個節(jié)點(diǎn),遍歷時間會呈指數(shù)級增加。
繪制:?在繪制階段,系統(tǒng)會再次從上到下遍歷樹,并且每個節(jié)點(diǎn)會依次在屏幕上繪制自身。
狀態(tài)
應(yīng)用中的狀態(tài)是指可以隨時間變化的任何值。這是一個非常寬泛的定義,從 Room 數(shù)據(jù)庫到類的變量,全部涵蓋在內(nèi)。
所有 Android 應(yīng)用都會向用戶顯示狀態(tài)。下面是 Android 應(yīng)用中的一些狀態(tài)示例:
????在無法建立網(wǎng)絡(luò)連接時顯示的信息提示控件。
????博文和相關(guān)評論。
????在用戶點(diǎn)擊按鈕時播放的漣漪效果。
????用戶可以在圖片上繪制的貼紙。
狀態(tài)和組合
由于 Compose 是聲明式工具集,因此更新它的唯一方法是通過新參數(shù)調(diào)用同一可組合項。這些參數(shù)是界面狀態(tài)的表現(xiàn)形式。每當(dāng)狀態(tài)更新時,都會發(fā)生重組。因此,TextField?不會像在基于 XML 的命令式視圖中那樣自動更新??山M合項必須明確獲知新狀態(tài),才能相應(yīng)地進(jìn)行更新。
可組合項中的狀態(tài)
可組合函數(shù)可以使用?remember?API 將對象存儲在內(nèi)存中。系統(tǒng)會在初始組合期間將由?remember?計算的值存儲在組合中,并在重組期間返回存儲的值。remember?既可用于存儲可變對象,又可用于存儲不可變對象。當(dāng)名為remember的可組合項從組合中移除后,它會忘記該對象。
mutableStateOf?會創(chuàng)建可觀察的?MutableState<T>,后者是與 Compose 運(yùn)行時集成的可觀察類型。
對?value?所做的任何更改都會安排重組讀取?value?的所有可組合函數(shù)。
在可組合項中聲明?MutableState?對象的方法有三種:
val mutableState = remember { mutableStateOf(default) }
var value by remember { mutableStateOf(default) }
val (value, setValue) = remember { mutableStateOf(default) }
這些聲明是等效的,以語法糖的形式針對狀態(tài)的不同用法提供。選擇的聲明應(yīng)該能夠在可組合項中生成可讀性最高的代碼。
by?委托語法需要以下導(dǎo)入:
import?androidx.compose.runtime.getValue
import?androidx.compose.runtime.setValue
可以將記住的值用作其他可組合項的參數(shù),甚至用作語句中的邏輯來更改要顯示的可組合項。
其他受支持的狀態(tài)類型
Compose 不要求使用?MutableState<T>?來保存狀態(tài);它支持其他可觀察類型。在讀取 Compose 中的其他可觀察類型之前,必須將其轉(zhuǎn)換為?State<T>,以便可組合項在狀態(tài)發(fā)生變化時自動重組。
Compose 附帶一些可以根據(jù) Android 應(yīng)用中使用的常見可觀察類型創(chuàng)建?State<T>?的函數(shù)。在使用這些集成之前,請先添加適當(dāng)?shù)?a target="_blank">工件,如下所述:
Flow:collectAsStateWithLifecycle()
collectAsStateWithLifecycle()?以生命周期感知型方式從?Flow?收集值,讓應(yīng)用能夠節(jié)省應(yīng)用資源。它表示 Compose?State?最新發(fā)出的值。建議使用此 API 作為在 Android 應(yīng)用中收集數(shù)據(jù)流的方法。
? ? ?build.gradle?文件中需要以下依賴項
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.2")
collectAsState?與?collectAsStateWithLifecycle?類似,因為它也會從?Flow?收集值并將其轉(zhuǎn)換為 Compose?State。
請為平臺通用代碼使用?collectAsState,而不要使用僅適用于 Android 的?collectAsStateWithLifecycle。
collectAsState?可在?compose-runtime?中使用,因此不需要其他依賴項。
observeAsState()?會開始觀察此?LiveData,并通過?State?表示其值。
build.gradle?文件中需要以下依賴項:
?? ? implementation("androidx.compose.runtime:runtime-livedata:1.6.8")
Compose 通過讀取?State?對象自動重組。如果在 Compose 中使用?LiveData?等其他可觀察類型,則應(yīng)在讀取該類型前,先將其轉(zhuǎn)換為?State。請務(wù)必在可組合項中轉(zhuǎn)換類型,并且使用?LiveData.observeAsState()?等可組合擴(kuò)展函數(shù)。
可使用的集成不限于上述幾種??梢詾?Jetpack Compose 構(gòu)建擴(kuò)展函數(shù),以便其讀取其他可觀察類型。如果應(yīng)用使用的是自定義可觀察類,請使用?produceState?API 對其進(jìn)行轉(zhuǎn)換,以生成?State<T>。
有狀態(tài)與無狀態(tài)
使用?remember?存儲對象的可組合項會創(chuàng)建內(nèi)部狀態(tài),使該可組合項有狀態(tài)。HelloContent?就是一個有狀態(tài)可組合項的示例,因為它會在內(nèi)部保持和修改自己的?name?狀態(tài)。在調(diào)用方不需要控制狀態(tài),并且不必自行管理狀態(tài)便可使用狀態(tài)的情況下,“有狀態(tài)”會非常有用。但是,具有內(nèi)部狀態(tài)的可組合項往往不易重復(fù)使用,也更難測試。
無狀態(tài)可組合項是指不保持任何狀態(tài)的可組合項。實(shí)現(xiàn)無狀態(tài)的一種簡單方法是使用狀態(tài)提升。
在開發(fā)可重復(fù)使用的可組合項時,通常想要同時提供同一可組合項的有狀態(tài)和無狀態(tài)版本。有狀態(tài)版本對于不關(guān)心狀態(tài)的調(diào)用方來說很方便,而無狀態(tài)版本對于需要控制或提升狀態(tài)的調(diào)用方來說是必要的。
狀態(tài)提升
Compose 中的狀態(tài)提升,是一種將狀態(tài)移至可組合項的調(diào)用方,使可組合項變成無狀態(tài)的模式。Jetpack Compose 中的常規(guī)狀態(tài)提升模式是將狀態(tài)變量替換為兩個參數(shù):
value: T:要顯示的當(dāng)前值
onValueChange: (T) -> Unit:請求更改值的事件,其中?T?是建議的新值
不過,并不局限于?onValueChange。如果更具體的事件適合可組合項,應(yīng)使用 lambda 定義事件。
以這種方式提升的狀態(tài)具有一些重要的屬性:
? ??單一可信來源:通過移動狀態(tài),而不是復(fù)制狀態(tài),可確保只有一個可信來源。這有助于避免 bug。
? ??封裝:只有有狀態(tài)可組合項能夠修改其狀態(tài)。完全是在內(nèi)部操作。
? ??可共享:可與多個可組合項共享提升的狀態(tài)。如果想在另一個可組合項中讀取?name,可以通過變量提升來做到這一點(diǎn)。
? ??可攔截:無狀態(tài)可組合項的調(diào)用方可以在更改狀態(tài)之前決定忽略或修改事件。
? ??分離:無狀態(tài)可組合項的狀態(tài)可以存儲在任何位置。例如,現(xiàn)在可以將?name?移入?ViewModel。
狀態(tài)下降、事件上升的這種模式稱為“單向數(shù)據(jù)流”。通過遵循單向數(shù)據(jù)流,可以將在界面中顯示狀態(tài)的可組合項與應(yīng)用中存儲和更改狀態(tài)的部分解耦。
提升狀態(tài)時,有三條規(guī)則可幫助弄清楚狀態(tài)應(yīng)去向何處:
狀態(tài)應(yīng)至少提升到使用該狀態(tài)(讀?。┑乃锌山M合項的最低共同父項。
狀態(tài)應(yīng)至少提升到它可以發(fā)生變化(寫入)的最高級別。
如果兩種狀態(tài)發(fā)生變化以響應(yīng)相同的事件,它們應(yīng)一起提升。
可以將狀態(tài)提升到高于這些規(guī)則要求的級別,但欠提升狀態(tài)會使遵循單向數(shù)據(jù)流變得困難或不可能。
提升狀態(tài)的場景
在 Compose 應(yīng)用中,提升界面狀態(tài)的場景取決于這是界面邏輯的需要還是業(yè)務(wù)邏輯的需要。
應(yīng)將界面狀態(tài)提升到讀取和寫入狀態(tài)的所有可組合項之間的最低共同祖先實(shí)體。應(yīng)使?fàn)顟B(tài)盡可能靠近其使用位置。通過狀態(tài)所有者,向使用者公開不可變狀態(tài)和事件,以修改狀態(tài)。
最低共同祖先實(shí)體也可以在組合之外。例如,因涉及業(yè)務(wù)邏輯而在?ViewModel?中提升狀態(tài)時。
界面狀態(tài)和界面邏輯的類型
界面狀態(tài)
界面狀態(tài)是描述界面的屬性。界面狀態(tài)有兩種類型:
屏幕界面狀態(tài)是需要在屏幕上顯示的內(nèi)容。例如,NewsUiState?類可以包含呈現(xiàn)界面所需的新聞報道和其他信息。由于該狀態(tài)包含應(yīng)用數(shù)據(jù),因此通常會與層次結(jié)構(gòu)中的其他層相關(guān)聯(lián)。
界面元素狀態(tài)是指界面元素的固有屬性,這些屬性會影響界面元素的呈現(xiàn)方式。界面元素可能處于顯示或隱藏狀態(tài),并且可能具有特定的字體、字號或顏色。在 Android View 中,View 會自行管理此狀態(tài)(因為它本身是有狀態(tài)的),并公開用于修改或查詢其狀態(tài)的方法。例如,TextView?類的?get?和?set?方法用于顯示該類的文本。在 Jetpack Compose 中,狀態(tài)在可組合項之外,甚至可以將狀態(tài)從可組合項附近提升到執(zhí)行調(diào)用的可組合函數(shù)或狀態(tài)容器中。例如,Scaffold?可組合項的?ScaffoldState。
邏輯
應(yīng)用中的邏輯可以是業(yè)務(wù)邏輯或界面邏輯:
業(yè)務(wù)邏輯決定著應(yīng)用數(shù)據(jù)的產(chǎn)品要求的實(shí)現(xiàn)。例如,在新聞閱讀器應(yīng)用中,當(dāng)用戶點(diǎn)按相應(yīng)按鈕時,就會為報道添加書簽。這種用于將書簽保存到文件或數(shù)據(jù)庫的邏輯通常放置在網(wǎng)域?qū)踊驍?shù)據(jù)層中。狀態(tài)容器通常通過調(diào)用這類層公開的方法,將此邏輯委托給相應(yīng)的層。
界面邏輯決定著如何在屏幕上顯示界面狀態(tài)。例如,在用戶選擇了某個類別時獲取正確的搜索欄提示、滾動至列表中的特定項,或者在用戶點(diǎn)擊某按鈕時便進(jìn)入特定屏幕的導(dǎo)航邏輯。
界面邏輯
當(dāng)界面邏輯需要讀取或?qū)懭霠顟B(tài)時,應(yīng)根據(jù)界面的生命周期,將狀態(tài)的作用域限定為界面。為了實(shí)現(xiàn)這一點(diǎn),應(yīng)在可組合函數(shù)中以正確的級別提升狀態(tài)?;蛘?,也可以在普通狀態(tài)容器類中執(zhí)行此操作,其作用域也限定為界面生命周期。
以可組合項作為狀態(tài)所有者
如果狀態(tài)和邏輯比較簡單,在可組合項中使用界面邏輯和界面元素狀態(tài)是一種不錯的方法??梢愿鶕?jù)需要將狀態(tài)保留在可組合項內(nèi)部或進(jìn)行提升。
不需要狀態(tài)提升:?并不總是需要提升狀態(tài)。當(dāng)其他可組合項不需要控制狀態(tài)時,可以將狀態(tài)保留在可組合項內(nèi)部。
在可組合項中提升:?如果需要與其他可組合項共用界面元素狀態(tài),并在不同位置將界面邏輯應(yīng)用到狀態(tài),則可在界面層次結(jié)構(gòu)中提升狀態(tài)所在的層次。這樣做會使可組合項的可重用性更高,并且更易于測試。
以普通狀態(tài)容器類作為狀態(tài)所有者
當(dāng)可組合項包含涉及界面元素的一個或多個狀態(tài)字段的復(fù)雜界面邏輯時,應(yīng)將這種責(zé)任委托給狀態(tài)容器,例如普通狀態(tài)容器類。這樣做更易于單獨(dú)對可組合項的邏輯進(jìn)行測試,還可以降低復(fù)雜性。該方法支持關(guān)注點(diǎn)分離原則:可組合項負(fù)責(zé)發(fā)出界面元素,而狀態(tài)容器包含界面邏輯和界面元素的狀態(tài)。
普通狀態(tài)容器類為可組合函數(shù)的調(diào)用方提供了一些便捷的函數(shù),這樣他們就無需自行編寫此邏輯。
這些普通類在組合中創(chuàng)建并記住。它們遵循可組合項的生命周期,因此可以采用 Compose 庫提供的類型,例如?rememberNavController()?或?rememberLazyListState()。
這種類型的一個示例是 Compose 中實(shí)現(xiàn)的?LazyListState?普通狀態(tài)容器類,用于控制?LazyColumn?或?LazyRow?的界面復(fù)雜性。
LazyListState?封裝用于存儲此界面元素的?scrollPosition?的?LazyColumn?的狀態(tài)。它還公開了修改滾動位置的方法,例如滾動到給定項。
ViewModel 作為狀態(tài)所有者
AAC ViewModels 在 Android 開發(fā)中的優(yōu)勢使其適用于提供對業(yè)務(wù)邏輯的訪問權(quán)限以及準(zhǔn)備要在屏幕上呈現(xiàn)的應(yīng)用數(shù)據(jù)。
在?ViewModel?中提升界面狀態(tài)時,會將其移出到組合之外。
ViewModel 不會作為組合的一部分進(jìn)行存儲。它們由框架提供,其作用域限定為?ViewModelStoreOwner,可以是 activity、fragment、導(dǎo)航圖或?qū)Ш綀D的目的地。如需詳細(xì)了解?ViewModel?作用域,請參閱相關(guān)文檔。
然后,ViewModel?是界面狀態(tài)的可信來源和最低共同祖先實(shí)體。
對于某些 Compose 界面元素狀態(tài),升級到?ViewModel?可能需要特別注意。例如,Compose 界面元素的某些狀態(tài)容器公開了修改狀態(tài)的方法。其中一些可能是觸發(fā)動畫的掛起函數(shù)。如果從作用域未限定于組合的?CoroutineScope?調(diào)用這些掛起函數(shù),則可能會拋出異常。
假設(shè)應(yīng)用抽屜的內(nèi)容是動態(tài)的,需要數(shù)據(jù)層關(guān)閉后從數(shù)據(jù)層中進(jìn)行提取和刷新。應(yīng)將抽屜狀態(tài)提升到?ViewModel,以便從狀態(tài)所有者調(diào)用此元素的界面和業(yè)務(wù)邏輯。
不過,從 Compose 界面使用?viewModelScope?調(diào)用?DrawerState?的?close()?方法會導(dǎo)致運(yùn)行時類型?IllegalStateException?的異常,并顯示消息:“a?MonotonicFrameClock?is not available in this?CoroutineContext”。
如需修復(fù)此問題,請使用作用域限定為組合的?CoroutineScope。這會在?CoroutineContext?中提供?MonotonicFrameClock,這是掛起函數(shù)正常運(yùn)行所必需的。
在 Compose 中保存界面狀態(tài)
根據(jù)要將狀態(tài)提升到什么位置和所需的邏輯,可以使用不同的 API 來存儲和恢復(fù)界面狀態(tài)。為了最有效地實(shí)現(xiàn)這一目的,每個應(yīng)用都組合使用 API。
任何 Android 應(yīng)用都可能會因重新創(chuàng)建 activity 或進(jìn)程而丟失界面狀態(tài)。此類狀態(tài)丟失可能是因以下事件造成的:
配置更改。除非手動處理配置變更,否則系統(tǒng)會銷毀并重新創(chuàng)建相應(yīng) activity。
系統(tǒng)發(fā)起的進(jìn)程終止。應(yīng)用位于后臺,而設(shè)備釋放了其他進(jìn)程要使用的資源(如內(nèi)存)。
在發(fā)生這些事件后保留狀態(tài)對于提供良好的用戶體驗至關(guān)重要。選擇要保留哪種狀態(tài)取決于應(yīng)用的唯一用戶體驗流程。根據(jù)最佳實(shí)踐,至少應(yīng)保留用戶輸入和導(dǎo)航相關(guān)狀態(tài)。這方面的例子包括:列表的滾動位置、用戶想詳細(xì)了解的項目的 ID、正在進(jìn)行的用戶偏好設(shè)置選擇或文本字段中的輸入。
在 Compose 中恢復(fù)狀態(tài)
rememberSaveable?API 的行為與?remember?類似,因為它會在重組時保留狀態(tài),還會在使用保存的實(shí)例狀態(tài)機(jī)制重新創(chuàng)建 activity 或進(jìn)程后保留狀態(tài)。例如,當(dāng)屏幕旋轉(zhuǎn)時,就會發(fā)生這種情況。如果 activity?被用戶完全關(guān)閉,rememberSaveable將不會保留狀態(tài)。例如,如果用戶從“最近使用的應(yīng)用”屏幕向上滑動當(dāng)前 activity,應(yīng)用不會保留狀態(tài)。
存儲狀態(tài)的方式
添加到?Bundle?的所有數(shù)據(jù)類型都會自動保存。如果要保存無法添加到?Bundle?的內(nèi)容,有以下幾種選擇:
? ??Parcelize:?最簡單的解決方案是向?qū)ο筇砑?@Parcelize?注解。對象將變?yōu)榭纱虬鼱顟B(tài)并且可以捆綁。
? ??MapSaver:?如果某種原因?qū)е?@Parcelize?不合適,可以使用?mapSaver?定義自己的規(guī)則,規(guī)定如何將對象轉(zhuǎn)換為系統(tǒng)可保存到?Bundle?的一組值。
? ??ListSaver:?為了避免需要為映射定義鍵,也可以使用?listSaver?并將其索引用作鍵
Compose 中的狀態(tài)容器
可以通過可組合函數(shù)本身管理簡單的狀態(tài)提升。但是,如果要跟蹤的狀態(tài)數(shù)增加,或者可組合函數(shù)中出現(xiàn)要執(zhí)行的邏輯,最好將邏輯和狀態(tài)事務(wù)委派給其他類(狀態(tài)容器)。
在鍵發(fā)生變化時重新觸發(fā) remember 計算
remember?API 經(jīng)常與?MutableState?結(jié)合使用:var?name by remember?{?mutableStateOf("")?}
因此,使用?remember?函數(shù)可使?MutableState?值在重組后繼續(xù)有效。
通常,remember?接受?calculation?lambda 參數(shù)。remember?會在首次運(yùn)行時調(diào)用?calculation?lambda 并存儲其結(jié)果。在重組期間,remember?會返回上次存儲的值。
除了緩存狀態(tài)之外,還可以使用?remember?將初始化或計算成本高昂的對象或操作結(jié)果存儲在組合中。因此,可能不會在每次重組時都重復(fù)進(jìn)行這種計算。
remember?會存儲該值,直到退出組合。不過,有一種方法可以讓緩存值失效。由于?remember?API 也接受?key?或?keys?參數(shù),因此,如果其中有任何鍵發(fā)生變化,那么下次函數(shù)重組時,remember?就會讓緩存失效并再次對 lambda 塊進(jìn)行計算。這種機(jī)制可控制組合中對象的生命周期。在輸入發(fā)生變化之前(而不是在記住的值退出組合之前),計算會一直有效。
Compose 會使用該類的?equals?實(shí)現(xiàn)來確定鍵是否已發(fā)生變化,并使存儲的值無效。
重組后使用鍵存儲狀態(tài)
rememberSaveable?API 是?remember?的封裝容器,可在?Bundle?中存儲數(shù)據(jù)。此 API 不僅能讓狀態(tài)在重組后保留下來,還能讓狀態(tài)在重新創(chuàng)建 activity 和系統(tǒng)發(fā)起的進(jìn)程終止后繼續(xù)留存。rememberSaveable?接收?input?參數(shù)的目的與?remember?接收?keys?的目的相同。只要輸入發(fā)生更改,緩存就會失效。下次函數(shù)重組時,rememberSaveable?會對 lambda 塊重新執(zhí)行計算。
狀態(tài)讀取
Compose 會自動跟蹤在系統(tǒng)讀取該值時正在執(zhí)行的操作。通過這項跟蹤,Compose 能夠在狀態(tài)值更改時重新執(zhí)行讀取程序,基于此實(shí)現(xiàn)了對狀態(tài)的觀察。
狀態(tài)通常是使用?mutableStateOf()?創(chuàng)建,然后通過以下兩種方式之一進(jìn)行訪問:直接訪問?value?屬性,或使用 Kotlin 屬性委托。
屬性委托在后臺使用“getter”和“setter”函數(shù)來訪問和更新狀態(tài)的?value。只有將相應(yīng)屬性作為值引用時,系統(tǒng)才會調(diào)用這些 getter 和 setter 函數(shù)(而不會在創(chuàng)建屬性時調(diào)用),因此上述兩種方法是等效的。
每個可以在讀取狀態(tài)發(fā)生更改時重新執(zhí)行的代碼塊都是一個重啟作用域。在不同階段內(nèi),Compose 會跟蹤狀態(tài)值的更改和重啟作用域。
分階段狀態(tài)讀取
如上所述,Compose 有 3 個主要階段,并且 Compose 會跟蹤在每個階段中讀取到的狀態(tài)。這樣一來,Compose 只需向需要對界面的每個受影響的元素執(zhí)行工作的特定階段發(fā)送通知即可。
創(chuàng)建和存儲狀態(tài)實(shí)例的位置與階段幾乎沒有什么關(guān)系,關(guān)鍵在于讀取狀態(tài)值的時間和位置。
第 1 階段:組合
@Composable?函數(shù)或 lambda 代碼塊中的狀態(tài)讀取會影響組合階段,并且可能會影響后續(xù)階段。當(dāng)狀態(tài)值發(fā)生更改時,Recomposer 會安排重新運(yùn)行所有要讀取相應(yīng)狀態(tài)值的可組合函數(shù)。請注意,如果輸入未更改,運(yùn)行時可能會決定跳過部分或全部可組合函數(shù)。
根據(jù)組合結(jié)果,Compose 界面會運(yùn)行布局和繪制階段。如果內(nèi)容保持不變,并且大小和布局也未更改,界面可能會跳過這些階段。
第 2 階段:布局
布局階段包含兩個步驟:測量和放置。測量步驟會運(yùn)行傳遞給?Layout?可組合項的測量 lambda、LayoutModifier?接口的?MeasureScope.measure?方法,等等。放置步驟會運(yùn)行?layout?函數(shù)的放置位置塊、Modifier.offset { … }?的 lambda 塊,等等。
每個步驟的狀態(tài)讀取都會影響布局階段,并且可能會影響繪制階段。當(dāng)狀態(tài)值發(fā)生更改時,Compose 界面會安排布局階段。如果大小或位置發(fā)生更改,界面還會運(yùn)行繪制階段。
更確切地說,測量步驟和放置步驟分別具有單獨(dú)的重啟作用域,這意味著,放置步驟中的狀態(tài)讀取不會在此之前重新調(diào)用測量步驟。不過,這兩個步驟通常是交織在一起的,因此在放置步驟中讀取的狀態(tài)可能會影響屬于測量步驟的其他重啟作用域。
第 3 階段:繪制
繪制代碼期間的狀態(tài)讀取會影響繪制階段。常見示例包括?Canvas()、Modifier.drawBehind?和?Modifier.drawWithContent。當(dāng)狀態(tài)值發(fā)生更改時,Compose 界面只會運(yùn)行繪制階段。
由于 Compose 會執(zhí)行局部狀態(tài)讀取跟蹤,因此可以在適當(dāng)階段讀取每個狀態(tài),從而盡可能降低需要執(zhí)行的工作量。
Compose 中的附帶效應(yīng)
附帶效應(yīng)是指發(fā)生在可組合函數(shù)作用域之外的應(yīng)用狀態(tài)的變化。由于可組合項的生命周期和屬性(例如不可預(yù)測的重組、以不同順序執(zhí)行可組合項的重組或可以舍棄的重組),可組合項在理想情況下應(yīng)該是無附帶效應(yīng)的。
不過,有時附帶效應(yīng)是必要的,例如,觸發(fā)一次性事件(例如顯示信息提示控件),或在滿足特定狀態(tài)條件時進(jìn)入另一個屏幕。這些操作應(yīng)從能感知可組合項生命周期的受控環(huán)境中調(diào)用。
由于效應(yīng)會在 Compose 中帶來各種可能性,所以很容易過度使用。確保在其中完成的工作與界面相關(guān),并且不會破壞單向數(shù)據(jù)流
自適應(yīng)界面本質(zhì)上是異步的,而 Jetpack Compose 會在 API 級別引入?yún)f(xié)程而非使用回調(diào)來解決此問題。
LaunchedEffect:在可組合項的作用域內(nèi)運(yùn)行掛起函數(shù)
如需在可組合項的生命周期內(nèi)執(zhí)行工作并能夠調(diào)用掛起函數(shù),請使用?LaunchedEffect?可組合項。當(dāng)?LaunchedEffect?進(jìn)入組合時,它會啟動一個協(xié)程,并將代碼塊作為參數(shù)傳遞。如果?LaunchedEffect?退出組合,協(xié)程將取消。如果使用不同的鍵重組?LaunchedEffect(請參閱下方的重啟效應(yīng)部分),系統(tǒng)將取消現(xiàn)有協(xié)程,并在新的協(xié)程中啟動新的掛起函數(shù)。
rememberCoroutineScope:獲取組合感知作用域,以在可組合項外啟動協(xié)程
由于?LaunchedEffect?是可組合函數(shù),因此只能在其他可組合函數(shù)中使用。為了在可組合項外啟動協(xié)程,但存在作用域限制,以便協(xié)程在退出組合后自動取消,請使用?rememberCoroutineScope。 此外,如果需要手動控制一個或多個協(xié)程的生命周期,請使用?rememberCoroutineScope,例如在用戶事件發(fā)生時取消動畫。
rememberCoroutineScope?是一個可組合函數(shù),會返回一個?CoroutineScope,該 CoroutineScope 綁定到調(diào)用它的組合點(diǎn)。調(diào)用退出組合后,作用域?qū)⑷∠?/p>
rememberUpdatedState:在效應(yīng)中引用值,在值發(fā)生更改時不應(yīng)重啟
當(dāng)其中一個鍵參數(shù)發(fā)生變化時,LaunchedEffect?會重啟。不過,在某些情況下,可能希望在效應(yīng)中捕獲某個值,但如果該值發(fā)生變化,不希望效應(yīng)重啟。為此,需要使用?rememberUpdatedState?來創(chuàng)建對可捕獲和更新的該值的引用。這種方法對于包含長期操作的效應(yīng)十分有用,因為重新創(chuàng)建和重啟這些操作可能代價高昂或令人望而卻步。
例如,假設(shè)應(yīng)用的?LandingScreen?在一段時間后消失。即使?LandingScreen?已重組,等待一段時間并發(fā)出時間已過通知的效應(yīng)也不應(yīng)該重啟。
DisposableEffect:需要清理的特效
對于需要在鍵發(fā)生變化或可組合項退出組合后進(jìn)行清理的附帶效應(yīng),請使用?DisposableEffect。如果?DisposableEffect?鍵發(fā)生變化,可組合項需要處理(執(zhí)行清理操作)其當(dāng)前效應(yīng),并通過再次調(diào)用效應(yīng)進(jìn)行重置。
例如,可能需要使用?LifecycleObserver,根據(jù)?Lifecycle?事件發(fā)送分析事件。如需在 Compose 中監(jiān)聽這些事件,請根據(jù)需要使用?DisposableEffect?注冊和取消注冊觀察器。
在?onDispose?中放置空塊并不是最佳做法。
SideEffect:將 Compose 狀態(tài)發(fā)布到非 Compose 代碼
如需與并非由 Compose 管理的對象共享 Compose 狀態(tài),請使用?SideEffect?可組合項。使用?SideEffect?可保證效應(yīng)在每次成功重組后執(zhí)行。另一方面,在保證成功重組前執(zhí)行效果是錯誤的,如果直接在可組合項中寫入效果,就會出現(xiàn)這種情況。
例如,分析庫可能允許通過將自定義元數(shù)據(jù)(在此示例中為“用戶屬性”)附加到所有后續(xù)分析事件,來細(xì)分用戶群體。如需將當(dāng)前用戶的用戶類型傳遞給分析庫,請使用?SideEffect?更新其值。
produceState:將非 Compose 狀態(tài)轉(zhuǎn)換為 Compose 狀態(tài)
produceState?會啟動一個協(xié)程,該協(xié)程將作用域限定為可將值推送到返回的?State?的組合。使用此協(xié)程將非 Compose 狀態(tài)轉(zhuǎn)換為 Compose 狀態(tài),例如將外部訂閱驅(qū)動的狀態(tài)(如?Flow、LiveData?或?RxJava)引入組合。
該制作工具在?produceState?進(jìn)入組合時啟動,在其退出組合時取消。返回的?State?沖突;設(shè)置相同的值不會觸發(fā)重組。
即使?produceState?創(chuàng)建了一個協(xié)程,它也可用于觀察非掛起的數(shù)據(jù)源。如需移除對該數(shù)據(jù)源的訂閱,請使用?awaitDispose?函數(shù)。
derivedStateOf:將一個或多個狀態(tài)對象轉(zhuǎn)換為另一種狀態(tài)
在 Compose 中,每次觀察到的狀態(tài)對象或可組合輸入出現(xiàn)變化時都會發(fā)生重組。狀態(tài)對象或輸入的變化頻率可能高于界面實(shí)際需要的更新頻率,從而導(dǎo)致不必要的重組。
當(dāng)可組合項輸入的變化頻率超過需要的重組頻率時,就應(yīng)該使用?derivedStateOf?函數(shù)。這種情況通常是指,某些內(nèi)容(例如滾動位置)頻繁變化,但可組合項只有在超過某個閾值時才需要對其做出響應(yīng)。derivedStateOf?會創(chuàng)建一個新的 Compose 狀態(tài)對象,可以觀察到該對象只會按照需要進(jìn)行更新。這樣,它的作用就與 Kotlin Flow?distinctUntilChanged()?運(yùn)算符類似。
snapshotFlow:將 Compose 的 State 轉(zhuǎn)換為 Flow
使用?snapshotFlow?將?State<T>?對象轉(zhuǎn)換為冷 Flow。snapshotFlow?會在收集到塊時運(yùn)行該塊,并發(fā)出從塊中讀取的?State?對象的結(jié)果。當(dāng)在?snapshotFlow?塊中讀取的?State?對象之一發(fā)生變化時,如果新值與之前發(fā)出的值不相等,F(xiàn)low 會向其收集器發(fā)出新值(此行為類似于?Flow.distinctUntilChanged?的行為)。
重啟效應(yīng)
Compose 中有一些效應(yīng)(如?LaunchedEffect、produceState?或?DisposableEffect)會采用可變數(shù)量的參數(shù)和鍵來取消運(yùn)行效應(yīng),并使用新的鍵啟動一個新的效應(yīng)。
這些 API 的典型形式是:EffectName(restartIfThisKeyChanges,?orThisKey,?orThisKey,?...)?{?block?}
由于此行為的細(xì)微差別,如果用于重啟效應(yīng)的參數(shù)不是適當(dāng)?shù)膮?shù),可能會出現(xiàn)問題:
如果重啟效應(yīng)次數(shù)不夠,可能會導(dǎo)致應(yīng)用出現(xiàn)錯誤。
如果重啟效應(yīng)次數(shù)過多,效率可能不高。
一般來說,效應(yīng)代碼塊中使用的可變和不可變變量應(yīng)作為參數(shù)添加到效應(yīng)可組合項中。除此之外,還可以添加更多參數(shù),以便強(qiáng)制重啟效應(yīng)。如果更改變量不應(yīng)導(dǎo)致效應(yīng)重啟,則應(yīng)將該變量封裝在?rememberUpdatedState?中。如果由于變量封裝在一個不含鍵的?remember?中使之沒有發(fā)生變化,則無需將變量作為鍵傳遞給效應(yīng)。
可以使用?true?等常量作為效應(yīng)鍵,使其遵循調(diào)用點(diǎn)的生命周期。它實(shí)際上具有有效的用例。但在這樣做之前,請審慎考慮,并確保確實(shí)需要這么做。
使用 CompositionLocal 將數(shù)據(jù)的作用域限定在局部
CompositionLocal?是通過組合隱式向下傳遞數(shù)據(jù)的工具。
通常情況下,在 Compose 中,數(shù)據(jù)以參數(shù)形式向下流經(jīng)整個界面樹傳遞給每個可組合函數(shù)。這會使可組合項的依賴項變?yōu)轱@式依賴項。但是,對于廣泛使用的常用數(shù)據(jù)(如顏色或類型樣式),這可能會很麻煩。為了支持無需將顏色作為顯式參數(shù)依賴項傳遞給大多數(shù)可組合項,Compose 提供了?CompositionLocal,可創(chuàng)建以樹為作用域的具名對象,這可以用作讓數(shù)據(jù)流經(jīng)界面樹的一種隱式方式。
CompositionLocal?元素通常在界面樹的某個節(jié)點(diǎn)以值的形式提供。該值可供其可組合項的后代使用,而無需在可組合函數(shù)中將?CompositionLocal?聲明為參數(shù)。
CompositionLocal?是 Material 主題在后臺使用的內(nèi)容。?MaterialTheme?是一個提供三個?CompositionLocal?實(shí)例(顏色、排版和形狀)的對象,可以在組合的任何部分檢索它們。具體來說,這些是可以通過?MaterialTheme?colors、shapes?和?typography?屬性訪問的?LocalColors、LocalShapes?和?LocalTypography?屬性。
CompositionLocal?實(shí)例的作用域限定為組合的一部分,因此可以在結(jié)構(gòu)樹的不同級別提供不同的值。CompositionLocal?的?current?值對應(yīng)于該組合部分中的某個祖先提供的最接近的值。
如需為?CompositionLocal?提供新值,請使用?CompositionLocalProvider?及其?provides?infix 函數(shù),該函數(shù)將?CompositionLocal?鍵與?value?相關(guān)聯(lián)。在訪問?CompositionLocal?的?current?屬性時,CompositionLocalProvider?的?content?lambda 將獲取提供的值。提供新值后,Compose 會重組讀取?CompositionLocal?的組合部分。
例如,LocalContentAlpha?CompositionLocal?包含用于文本和圖標(biāo)的首選內(nèi)容 Alpha 值,以強(qiáng)調(diào)或弱化界面的不同部分。
使用?CompositionLocal?的另一個關(guān)鍵信號是該參數(shù)為橫切參數(shù)且中間層的實(shí)現(xiàn)不應(yīng)知道該參數(shù)的存在,因為讓這些中間層知道會限制可組合項的功用。例如,對 Android 權(quán)限的查詢是由?CompositionLocal?在后臺提供的。媒體選擇工具可組合項可以添加新功能,即訪問設(shè)備上受權(quán)限保護(hù)的內(nèi)容而無需更改其 API,并且需要媒體選擇工具的調(diào)用方知道從環(huán)境中使用的這一新增的上下文。
但是,CompositionLocal?并非始終是最好的解決方案。不建議過度使用?CompositionLocal,因為它存在一些缺點(diǎn):
CompositionLocal?使得可組合項的行為更難推斷。在創(chuàng)建隱式依賴項時,使用這些依賴項的可組合項的調(diào)用方需要確保為每個?CompositionLocal?提供一個值。
此外,該依賴項可能沒有明確的可信來源,因為它可能會在組合中的任何部分發(fā)生改變。因此,在出現(xiàn)問題時調(diào)試應(yīng)用可能更具有挑戰(zhàn)性因為需要向上查看組合,了解提供?current?值的位置。Android Studio 中的“Find usages”或?Compose 布局檢查器等工具提供了足夠的信息來緩解這個問題。
決定是否使用?CompositionLocal
CompositionLocal?應(yīng)具有合適的默認(rèn)值。如果沒有默認(rèn)值,必須保證開發(fā)者極其難陷入不提供?CompositionLocal?值的狀況。如果創(chuàng)建測試或預(yù)覽使用該?CompositionLocal?的可組合項時始終需要顯式提供默認(rèn)值,那么不提供默認(rèn)值可能會導(dǎo)致問題并帶來糟糕的體驗。
有些概念并非以樹或子層次結(jié)構(gòu)為作用域,請避免對這些概念使用?CompositionLocal。建議使用?CompositionLocal?的情況為:其可能會被任何(而非少數(shù)幾個)后代使用。
一種錯誤做法的示例是創(chuàng)建包含特定屏幕的?ViewModel?的?CompositionLocal,以便該屏幕中的所有可組合項都可以獲取對?ViewModel?的引用來執(zhí)行某些邏輯。這是一種錯誤做法,因為并非特定界面樹下的所有可組合項都需要知道?ViewModel。最佳做法是遵循ni的模式,只向可組合項傳遞所需信息。這樣做會使可組合項的可重用性更高,并且更易于測試。
創(chuàng)建?CompositionLocal
有兩個 API 可用于創(chuàng)建?CompositionLocal:
compositionLocalOf:在重組期間更改提供的值只會使讀取其?current?值的內(nèi)容無效。
staticCompositionLocalOf:與?compositionLocalOf?不同,Compose 不會跟蹤?staticCompositionLocalOf?的讀取。更改該值會導(dǎo)致提供?CompositionLocal?的整個?content?lambda 被重組,而不僅僅是在組合中讀取?current?值的位置。
如果為?CompositionLocal?提供的值發(fā)生更改的可能性微乎其微或永遠(yuǎn)不會更改,使用?staticCompositionLocalOf?可提高性能。
為?CompositionLocal?提供值
CompositionLocalProvider?可組合項可將值綁定到給定層次結(jié)構(gòu)的?CompositionLocal?實(shí)例。如需為?CompositionLocal?提供新值,請使用?provides?infix 函數(shù),該函數(shù)將?CompositionLocal?鍵與?value?相關(guān)聯(lián)
使用?CompositionLocal
CompositionLocal.current?返回由最接近的?CompositionLocalProvider提供的值
需考慮的替代方案
對于某些用例,CompositionLocal?可能是一種過度的解決方案。如果用例不符合決定是否使用 CompositionLocal?中指定的條件,其他解決方案可能更適合
傳遞顯式參數(shù)
顯式使用可組合項的依賴項是一種很好的習(xí)慣。建議僅傳遞所需可組合項。為了鼓勵分離和重用可組合項,每個可組合項包含的信息應(yīng)該盡可能少。
控制反轉(zhuǎn)
另一種避免將不必要的依賴項傳遞給可組合項的方法是采用控制反轉(zhuǎn)方式。不是由后代接受依賴項來執(zhí)行某些邏輯,而是由父級接受依賴項來執(zhí)行某些邏輯。
將子級與其直接祖先實(shí)體分離。祖先實(shí)體可組合項往往越來越復(fù)雜,這樣就可以使更低級別的可組合項更靈活。
同樣,可以用相同的方式使用?@Composable?內(nèi)容 lambda,以獲得相同的優(yōu)勢