CompositionLocal
是通過組合隱式向下傳遞數(shù)據(jù)的工具
主要學(xué)習(xí)內(nèi)容
- 了解什么是
CompositionLocal
- 創(chuàng)建自己的
CompositionLocal
- 何時(shí)使用
CompositionLocal
顯示傳參與隱式傳參
顯示傳參
@Test
fun Surface() {
val textColor = "紅色"
Column(textColor)
}
fun Column(textColor: String) {
println("Column color:$textColor")
Button(textColor)
}
fun Button(textColor: String) {
println("Button color:$textColor")
Text(textColor)
}
fun Text(textColor: String) {
println("Text color:$textColor")
}
顯示傳參中數(shù)據(jù)以參數(shù)的形式向下流經(jīng)整個(gè)調(diào)用過程洞难,可以看到為了傳遞textColor
參數(shù)需要每個(gè)調(diào)用方都增加textColor
參數(shù)
隱式傳參
var textColor = "紅色"
@Test
fun Surface() {
Column()
}
fun Column() {
println("Column color:$textColor")
Button()
}
fun Button() {
println("Button color:$textColor")
Text()
}
fun Text() {
println("Text color:$textColor")
}
隱式傳參中數(shù)據(jù)以共有數(shù)據(jù)的形式傳遞,不需要額外參數(shù)
注:此時(shí)的
Surface
、Column
等只是普通函數(shù)
函數(shù)內(nèi)修改值
顯示傳參
@Test
fun Surface() {
val textColor = "紅色"
Column(textColor)
println("----------")
Column(textColor)
}
//未修改Column,省略Column代碼
fun Button(textColor: String) {
textColor="黑色"
println("Button color:$textColor")
Text(textColor)
}
//未修改Text,省略Text代碼
Column color:紅色
Button color:黑色
Text color:黑色
----------
Column color:紅色
Button color:黑色
Text color:黑色
顯示傳參中丰泊,修改數(shù)據(jù)的值不會影響下一次調(diào)用,即數(shù)據(jù)隔離效果。而且可以影響下游的值
隱式傳參
var textColor = "紅色"
@Test
fun Surface() {
Column()
println("----------")
Column()
}
//未修改Column索守,省略Column代碼
fun Button() {
textColor="黑色"
println("Button using:$textColor")
Text()
}
//未修改Text,省略Text代碼
Column color:紅色
Button color:黑色
Text color:黑色
----------
Column color:黑色
Button color:黑色
Text color:黑色
第一次調(diào)用沒有問題抑片,但第二次調(diào)用時(shí)textColor
就全為黑色卵佛,即隱式傳參中值的修改不是局部的,會導(dǎo)致其他地方對于textColor
的使用發(fā)生變化
那么若我們只想在Button
及其子元素中修改textColor
值敞斋,在其他地方仍為原來的值該怎么辦呢截汪?
我們可以記錄textColor
原本的值,在使用完后再將原本的值重新賦值給textColor
fun Button() {
val origin = textColor
textColor = "黑色"
println("Button color:$textColor")
Text()
textColor = origin
}
在kotlin中我們還可以進(jìn)一步封裝該過程
fun Button() {
provider("黑色") {
Text()
println("Button color:$textColor")
}
}
fun provider(changed: String, action: () -> Unit) {
val origin = textColor
textColor = changed
action()
textColor = origin
}
雖然這個(gè)代碼比較簡易植捎,但是與
CompositionLocal
的原理大同小異衙解。可以基于這個(gè)大致理解CompositionLocal
焰枢,若想深入理解CompositionLocal
需要去查閱源碼
簡介
在 Compose 中往往可組合項(xiàng)調(diào)用另一個(gè)可組合項(xiàng)丢郊,若所有數(shù)據(jù)都以參數(shù)形式向下流經(jīng)整個(gè)界面樹傳遞給每個(gè)可組合函數(shù)盔沫,那么對于廣泛使用的常用數(shù)據(jù)(如顏色或類型樣式),這可能會很麻煩枫匾,無論是在編寫代碼還是維護(hù)代碼時(shí)
為了支持無需將顏色作為顯式參數(shù)依賴項(xiàng)傳遞給大多數(shù)可組合項(xiàng)架诞,Compose 提供了 CompositionLocal
,可讓您創(chuàng)建以樹為作用域的具名對象干茉,這可以用作讓數(shù)據(jù)流經(jīng)界面樹的一種隱式方式
CompositionLocal
元素通常在界面樹的某個(gè)節(jié)點(diǎn)以值的形式提供谴忧。該值可供其可組合項(xiàng)的后代使用,而無需在可組合函數(shù)中將 CompositionLocal
聲明為參數(shù)
在MaterialTheme
對象中提供了三個(gè) CompositionLocal
實(shí)例角虫,即 colors沾谓、typography 和 shapes。我們可以在之后的可組合函數(shù)中檢索這些實(shí)例
具體來說戳鹅,這些是可以通過
MaterialTheme
colors
均驶、shapes
和typography
屬性訪問的LocalColors
、LocalShapes
和LocalTypography
屬性
@Composable
fun MyApp() {
MaterialTheme {
SomeTextLabel("CompositionLocal")
}
}
@Composable
fun SomeTextLabel(labelText: String) {
Text(
text = labelText,
// 通過LocalColors隱式傳值
color = MaterialTheme.colors.primary
)
}
CompositionLocal
實(shí)例的作用域限定為組合的一部分枫虏,因此您可以在結(jié)構(gòu)樹的不同級別提供不同的值妇穴。CompositionLocal
的 current
值對應(yīng)于該組合部分中最接近的祖先提供的值
在Column
中修改了CompositionLocal
的值,于是在組合樹中分為了兩個(gè)作用域范圍
ListItem
中獲取CompositionLocal
的current
值就對應(yīng)于Column
提供的值
BottomNavigation
中獲取CompositionLocal
的current
值就對應(yīng)于Scaffold
提供的值隶债,而不是Column
提供的值腾它,因?yàn)?code>Column不是BottomNavigation
的祖先節(jié)點(diǎn)而是兄弟節(jié)點(diǎn)
如需為 CompositionLocal
提供新值,請使用 CompositionLocalProvider
及其 provides
infix 函數(shù)死讹,該函數(shù)將 CompositionLocal
鍵與 value
相關(guān)聯(lián)瞒滴。在訪問 CompositionLocal
的 current
屬性時(shí),CompositionLocalProvider
的 content
lambda 將獲取提供的值赞警。提供新值后妓忍,Compose 會重組讀取 CompositionLocal
的組合部分
@Composable
fun CompositionLocalExample() {
//MaterialTheme中LocalContentAlpha值默認(rèn)為ContentAlpha.high
MaterialTheme {
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
//通過使用其`current`屬性獲取`CompositionLocal`當(dāng)前值
Text("This Text also uses the medium value:${LocalContentAlpha.current}")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProvider可以跨函數(shù)作用
Text("This Text uses the disabled alpha now")
}
Material 可組合項(xiàng)會在內(nèi)部使用CompositionLocal
,我們可以通過使用其current
屬性獲取CompositionLocal
當(dāng)前值
注意:
CompositionLocal
對象或常量通常帶有Local
前綴愧旦,以便在 IDE 中利用自動填充功能提高可檢測性
自定義CompositionLocal
CompositionLocal
是通過組合隱式向下傳遞數(shù)據(jù)的工具
使用 CompositionLocal
的另一個(gè)關(guān)鍵點(diǎn)是該參數(shù)是橫切參數(shù)(隱式傳參)且中間層的實(shí)現(xiàn)不需要知道該參數(shù)的存在单默。例如,對 Android 權(quán)限的查詢是由 CompositionLocal
在后臺提供的忘瓦。媒體選擇器可組合項(xiàng)繼續(xù)添加新功能,無需根據(jù)權(quán)限是否獲取去修改其API引颈。只需要媒體選擇器的調(diào)用方知道權(quán)限的獲取情況
這里吐槽一句耕皮,辣雞谷歌機(jī)翻(╯' - ')╯︵ ┻━┻
但是,CompositionLocal
并非始終是最好的解決方案蝙场。不建議過度使用 CompositionLocal
凌停,其存在一些缺點(diǎn):
-
CompositionLocal
使得可組合項(xiàng)的行為更難推斷Material 組件中大量使用
CompositionLocal
方式傳遞值,若不熟悉這些組件則很難判斷出組件最后呈現(xiàn)效果和應(yīng)修改那些CompositionLocal
值 -
在創(chuàng)建隱式依賴項(xiàng)時(shí)售滤,使用這些依賴項(xiàng)的可組合項(xiàng)的調(diào)用方需要確保為每個(gè)
CompositionLocal
提供一個(gè)值該依賴項(xiàng)可能沒有明確的可信來源罚拟,因?yàn)樗赡軙诮M合中的任何部分發(fā)生改變台诗,會使得測試時(shí)難度變高
CompositionLocal
非常適合基礎(chǔ)架構(gòu),而且 Jetpack Compose 大量使用該工具
使用條件
CompositionLocal
應(yīng)具有合適的默認(rèn)值赐俗。如果沒有默認(rèn)值拉队,在開發(fā)時(shí)很容易陷入不提供CompositionLocal
導(dǎo)致的異常
如果創(chuàng)建測試或預(yù)覽使用該 CompositionLocal
的可組合項(xiàng)時(shí)也需要顯式提供默認(rèn)值,那么不提供默認(rèn)值不僅會帶來問題還會造成了糟糕的使用體驗(yàn)
有些概念并非以樹或子層次結(jié)構(gòu)為作用域阻逮,請避免對這些概念使用 CompositionLocal
粱快,建議使用CompositionLocal
的情況為:其可能會被任何(而非少數(shù)幾個(gè))后代使用
一種錯誤做法的示例是創(chuàng)建在特定界面使用的
ViewModel
的CompositionLocal
,以便該屏幕中的所有可組合項(xiàng)都可以獲取ViewModel
來執(zhí)行某些邏輯因?yàn)樘囟ń缑嫦虏⒉皇撬锌山M合項(xiàng)都需要知道
ViewModel
叔扼。最佳做法是使用狀態(tài)向下傳遞而事件向上傳遞的單向數(shù)據(jù)流模式事哭,或只向可組合項(xiàng)傳遞所需信息。這樣做會使可組合項(xiàng)的可重用性更高瓜富,并且更易于測試
創(chuàng)建CompositionLocal
有兩個(gè) API 可用于創(chuàng)建 CompositionLocal
:
-
compositionLocalOf
:如果更改提供的值鳍咱,會使讀取其current
值的組件發(fā)生重組 -
staticCompositionLocalOf
:與compositionLocalOf
不同,Compose 不會跟蹤staticCompositionLocalOf
的讀取与柑。更改該值會導(dǎo)致提供CompositionLocal
的整個(gè)content
lambda 被重組谤辜,而不僅僅是在組合中讀取current
值的位置
如果為 CompositionLocal
提供的值發(fā)生更改的可能性微乎其微或永遠(yuǎn)不會更改,使用 staticCompositionLocalOf
可提高性能
例:
data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)
// 定義一個(gè)帶有默認(rèn)值的compostionlocal全局對象
// 這個(gè)實(shí)例可以被應(yīng)用中的所有可組合項(xiàng)訪問
val LocalElevations = compositionLocalOf { Elevations() }
為CompositionLocal提供值
CompositionLocalProvider
可組合項(xiàng)可將值綁定到給定層次結(jié)構(gòu)的 CompositionLocal
實(shí)例仅胞。如需為 CompositionLocal
提供新值每辟,請使用 provides
infix 函數(shù),該函數(shù)將 CompositionLocal
鍵與 value
相關(guān)聯(lián)干旧,如下所示:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 基于系統(tǒng)主題創(chuàng)建不同的Elevations
val elevations = if (isSystemInDarkTheme()) {
Elevations(card = 1.dp, default = 1.dp)
} else {
Elevations(card = 0.dp, default = 0.dp)
}
// 將elevations賦值給LocalElevations
CompositionLocalProvider(LocalElevations provides elevations) {
// ... Content goes here ...
// 可組合項(xiàng)都可以通過LocalElevations.current獲取到elevations實(shí)例
}
}
}
}
使用CompositionLocalProvider
CompositionLocal.current
返回由最接近的 CompositionLocalProvider
(其向該 CompositionLocal
提供一個(gè)值)提供的值:
@Composable
fun SomeComposable() {
Card(elevation = LocalElevations.current.card) {
// Content
}
}
替換方案
在某些情況中渠欺,CompositionLocal
可能是一種過度的解決方案。如果您的用例不符合CompositionLocal
使用條件椎眯,其他解決方案可能更適合您的用例
傳遞顯示參數(shù)
顯式使用可組合項(xiàng)的依賴項(xiàng)是一種很好的習(xí)慣挠将。建議僅傳遞所需可組合項(xiàng)。為了鼓勵分離和重用可組合項(xiàng)编整,每個(gè)可組合項(xiàng)包含的信息應(yīng)該可能少
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
MyDescendant(myViewModel.data)
}
// ? 不要傳遞整個(gè)對象! 應(yīng)該只傳遞需要的部分
// 同樣也不要使用 CompositionLocal 隱式傳遞 ViewModel
@Composable
fun MyDescendant(myViewModel: MyViewModel) { ... }
// 只傳遞需要的部分
@Composable
fun MyDescendant(data: DataToDisplay) {
// Display data
}
控制反轉(zhuǎn)
即不是由后代接受依賴項(xiàng)來執(zhí)行某些邏輯舔稀,而是由父級接受依賴項(xiàng)來執(zhí)行某些邏輯,也就是狀態(tài)向下傳遞而事件向上傳遞的單向數(shù)據(jù)流模式
在以下示例中掌测,后代需要觸發(fā)請求以加載某些數(shù)據(jù):
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
...
MyDescendant(myViewModel)
}
@Composable
fun MyDescendant(myViewModel: MyViewModel) {
Button(onClick = { myViewModel.loadData() }) {
Text("Load data")
}
}
此時(shí)我們可以考慮將MyDescendant
中Button
的點(diǎn)擊事件上傳内贮,在MyComposable
中執(zhí)行myViewModel.loadData()
,即事件上傳
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
...
ReusableLoadDataButton(
onLoadClick = {
myViewModel.loadData()
}
)
}
@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
Button(onClick = onLoadClick) {
Text("Load data")
}
}
此方法將子級與其直接祖先實(shí)體分離開來將子級與其直接祖先實(shí)體分離汞斧。雖然祖先實(shí)體可組合項(xiàng)往往越來越復(fù)雜夜郁,這樣就可以使更低級別的可組合項(xiàng)更靈活