Jeptpack Compose 官網(wǎng)教程學(xué)習(xí)筆記(四)番外-CompositionLocal

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í)的SurfaceColumn等只是普通函數(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均驶、shapestypography 屬性訪問的 LocalColorsLocalShapesLocalTypography 屬性

@Composable
fun MyApp() {
    MaterialTheme {
        SomeTextLabel("CompositionLocal")
    }
}

@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // 通過LocalColors隱式傳值
        color = MaterialTheme.colors.primary
    )
}

CompositionLocal 實(shí)例的作用域限定為組合的一部分枫虏,因此您可以在結(jié)構(gòu)樹的不同級別提供不同的值妇穴。CompositionLocalcurrent 值對應(yīng)于該組合部分中最接近的祖先提供的值

CompositionLocal作用域分析.png

Column中修改了CompositionLocal的值,于是在組合樹中分為了兩個(gè)作用域范圍

ListItem中獲取CompositionLocalcurrent 值就對應(yīng)于Column提供的值

BottomNavigation中獲取CompositionLocalcurrent 值就對應(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)瞒滴。在訪問 CompositionLocalcurrent 屬性時(shí),CompositionLocalProvidercontent 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")
}
預(yù)覽效果

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)建在特定界面使用的 ViewModelCompositionLocal,以便該屏幕中的所有可組合項(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í)我們可以考慮將MyDescendantButton的點(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)更靈活

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末啤它,一起剝皮案震驚了整個(gè)濱河市谚咬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锻霎,老刑警劉巖庙睡,帶你破解...
    沈念sama閱讀 211,348評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件事富,死亡現(xiàn)場離奇詭異技俐,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)统台,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評論 2 385
  • 文/潘曉璐 我一進(jìn)店門雕擂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饺谬,你說我怎么就攤上這事捂刺。” “怎么了募寨?”我有些...
    開封第一講書人閱讀 156,936評論 0 347
  • 文/不壞的土叔 我叫張陵族展,是天一觀的道長。 經(jīng)常有香客問我拔鹰,道長仪缸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,427評論 1 283
  • 正文 為了忘掉前任列肢,我火速辦了婚禮恰画,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瓷马。我一直安慰自己拴还,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評論 6 385
  • 文/花漫 我一把揭開白布欧聘。 她就那樣靜靜地躺著片林,像睡著了一般。 火紅的嫁衣襯著肌膚如雪怀骤。 梳的紋絲不亂的頭發(fā)上费封,一...
    開封第一講書人閱讀 49,785評論 1 290
  • 那天,我揣著相機(jī)與錄音蒋伦,去河邊找鬼弓摘。 笑死,一個(gè)胖子當(dāng)著我的面吹牛痕届,可吹牛的內(nèi)容都是我干的韧献。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼研叫,長吁一口氣:“原來是場噩夢啊……” “哼锤窑!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蓝撇,我...
    開封第一講書人閱讀 37,696評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎陈莽,沒想到半個(gè)月后渤昌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虽抄,經(jīng)...
    沈念sama閱讀 44,141評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評論 2 327
  • 正文 我和宋清朗相戀三年独柑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了迈窟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,625評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡忌栅,死狀恐怖车酣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情索绪,我是刑警寧澤湖员,帶...
    沈念sama閱讀 34,291評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站瑞驱,受9級特大地震影響娘摔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜唤反,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評論 3 312
  • 文/蒙蒙 一凳寺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧彤侍,春花似錦肠缨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至般哼,卻和暖如春吴汪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蒸眠。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工漾橙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人楞卡。 一個(gè)月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓霜运,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蒋腮。 傳聞我的和親對象是個(gè)殘疾皇子淘捡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評論 2 348

推薦閱讀更多精彩內(nèi)容