Jetpack (七)Android Compose 編程思想<4>

Jetpack Compose 是一個適用于 Android 的新式聲明性界面工具包。Compose 提供聲明性 API,可在不以命令方式改變前端視圖的情況下呈現(xiàn)應(yīng)用界面,從而使編寫和維護(hù)應(yīng)用界面變得更加容易。它的含義對應(yīng)用設(shè)計非常重要。

1. 聲明性編程范式

長期以來,Android 視圖層次結(jié)構(gòu)一直可以表示為界面View樹。由于應(yīng)用的狀態(tài)會因用戶交互等因素而發(fā)生變化,因此界面層次結(jié)構(gòu)需要進(jìn)行更新以顯示當(dāng)前數(shù)據(jù)。最常見的界面更新方式是使用 findViewById()等函數(shù)遍歷樹,并通過調(diào)用 button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap) 等方法更改節(jié)點。這些方法會改變View的內(nèi)部狀態(tài)。

手動操縱視圖會提高出錯的可能性。如果一條數(shù)據(jù)在多個位置呈現(xiàn),很容易忘記更新顯示它的某個視圖。此外,當(dāng)兩項更新以意外的方式發(fā)生沖突時,也很容易造成異常狀態(tài)。例如,某項更新可能會嘗試設(shè)置剛剛從界面中移除的節(jié)點的值。一般來說,軟件維護(hù)復(fù)雜性會隨著需要更新的視圖數(shù)量而增長。

在過去的幾年中,整個行業(yè)已開始轉(zhuǎn)向聲明性界面模型,該模型大大簡化了與構(gòu)建和更新界面關(guān)聯(lián)的工程設(shè)計。該技術(shù)的工作原理是在概念上從頭開始重新生成整個屏幕,然后僅執(zhí)行必要的更改。此方法可避免手動更新有狀態(tài)視圖層次結(jié)構(gòu)的復(fù)雜性。Compose 是一個聲明性界面框架。

重新生成整個屏幕所面臨的一個難題是,在時間、計算能力和電池用量方面可能成本高昂。為了減輕這一成本,Compose 會智能地選擇在任何給定時間需要重新繪制界面的哪些部分。這會對您設(shè)計界面組件的方式有一定影響,如下面重組中所述。

2. 簡單的可組合函數(shù)

在Compose中我們稱View 為 微件,但是和View 不完全一致。

一個簡單的可組合函數(shù),系統(tǒng)向它傳遞了數(shù)據(jù),它使用該數(shù)據(jù)在屏幕上呈現(xiàn)文本微件。

  • 此函數(shù)帶有 @Composable 注釋。所有可組合函數(shù)都必須帶有此注釋;此注釋可告知 Compose 編譯器:此函數(shù)旨在將數(shù)據(jù)轉(zhuǎn)換為界面。

  • 此函數(shù)接受數(shù)據(jù)。可組合函數(shù)可以接受一些參數(shù),這些參數(shù)可讓應(yīng)用邏輯描述界面。在本例中,我們的微件接受一個 String,因此它可以按名稱問候用戶。

  • 此函數(shù)可以在界面中顯示文本。為此,它會調(diào)用 Text() 可組合函數(shù),該函數(shù)實際上會創(chuàng)建文本界面元素。可組合函數(shù)通過調(diào)用其他可組合函數(shù)來發(fā)出界面層次結(jié)構(gòu)。

  • 此函數(shù)不會返回任何內(nèi)容。發(fā)出界面的 Compose 函數(shù)不需要返回任何內(nèi)容,因為它們描述所需的屏幕狀態(tài),而不是構(gòu)造界面微件。

  • 此函數(shù)快速、冪等且沒有副作用。

冪等,在我的kotlin文章也有介紹,關(guān)于副作用等
在某二元運算下,冪等元素是指被自己重復(fù)運算(或?qū)τ诤瘮?shù)是為復(fù)合)的結(jié)果等于它自己的元素。例如,乘法下僅有兩個冪等實數(shù),為0和1。
某一元運算為冪等的時,其作用在任一元素兩次后會和其作用一次的結(jié)果相同。例如,高斯符號便是冪等的。
一元運算的定義是二元運算定義的特例
簡單理解 f(f(x)) = f(x) 可以自行查詢 維基百科 或者 百度百科

  • 使用同一參數(shù)多次調(diào)用此函數(shù)時,它的行為方式相同,并且它不使用其他值,如全局變量或?qū)?random() 的調(diào)用。
  • 此函數(shù)描述界面而沒有任何副作用,如修改屬性或全局變量。

一般來說,出于重組部分所述的原因,所有可組合函數(shù)都應(yīng)使用這些屬性來編寫。

3. 聲明性范式轉(zhuǎn)變

在許多面向?qū)ο蟮拿钍浇缑婀ぞ甙?,您可以通過實例化View樹來初始化界面。您通常通過膨脹 XML 布局文件來實現(xiàn)此目的。每個View都維護(hù)自己的內(nèi)部狀態(tài),并且提供 getter 和 setter 方法,允許應(yīng)用邏輯與View進(jìn)行交互。

在 Compose 的聲明性方法中,微件相對無狀態(tài),并且不提供 setter 或 getter 函數(shù)。實際上,微件不會以對象形式提供。您可以通過調(diào)用帶有不同參數(shù)的同一可組合函數(shù)來更新界面。這使得向架構(gòu)模式(如 ViewModel)提供狀態(tài)變得很容易,如應(yīng)用架構(gòu)指南中所述。然后,可組合項負(fù)責(zé)在每次可觀察數(shù)據(jù)更新時將當(dāng)前應(yīng)用狀態(tài)轉(zhuǎn)換為界面。

PS: 這里我們想到,F(xiàn)lutter中的有狀態(tài)組件和無狀態(tài)組件。還有Kotlin 底層默認(rèn)實現(xiàn)setter和getter。

上圖說明:應(yīng)用邏輯為頂級可組合函數(shù)提供數(shù)據(jù)。該函數(shù)通過調(diào)用其他可組合函數(shù)來使用這些數(shù)據(jù)描述界面,將適當(dāng)?shù)臄?shù)據(jù)傳遞給這些可組合函數(shù),并沿層次結(jié)構(gòu)向下傳遞數(shù)據(jù)。

當(dāng)用戶與界面交互時,界面會發(fā)起 onClick 等事件。這些事件應(yīng)通知應(yīng)用邏輯,應(yīng)用邏輯隨后可以改變應(yīng)用的狀態(tài)。當(dāng)狀態(tài)發(fā)生變化時,系統(tǒng)會使用新數(shù)據(jù)再次調(diào)用可組合函數(shù)。這會導(dǎo)致重新繪制界面元素,此過程稱為“重組”。

PS: 這里我猜測,Viewmodel,LifeCycle 等狀態(tài)機(jī)模式類似,根據(jù)狀態(tài)動態(tài)更新,也是MVVM,F(xiàn)Lux等類似的機(jī)制,沒有看底層源碼,先這樣理解,以后深入研究在驗證猜想。

上圖說明:用戶與界面元素進(jìn)行了交互,導(dǎo)致觸發(fā)一個事件。應(yīng)用邏輯響應(yīng)該事件,然后系統(tǒng)根據(jù)需要使用新參數(shù)自動再次調(diào)用可組合函數(shù)。

動態(tài)內(nèi)容

由于可組合函數(shù)是用 Kotlin 而不是 XML 編寫的,因此它們可以像其他任何 Kotlin 代碼一樣動態(tài)。例如,假設(shè)您想要構(gòu)建一個界面,用來問候一些用戶:

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}

此函數(shù)接受名稱的列表,并為每個用戶生成一句問候語??山M合函數(shù)可能非常復(fù)雜。可以使用 if 語句來確定是否要顯示特定的界面元素。可以使用循環(huán)??梢哉{(diào)用輔助函數(shù)。使用底層語言的全部靈活性。這種功能和靈活性是 Jetpack Compose 的主要優(yōu)勢之一。

PS:其實就是用代碼控制,去除模板xml的做法,以前需要自己寫邏輯實現(xiàn),現(xiàn)在已經(jīng)實現(xiàn)好了,我們只關(guān)心自己的邏輯就好,所以才會強(qiáng)大好用。最主要是靈活了。

4. 重組

終于學(xué)習(xí)到這個概念。

在命令式界面模型中,如需更改某個View,您可以在該View上調(diào)用 setter 以更改其內(nèi)部狀態(tài)。在 Compose 中,您可以使用新數(shù)據(jù)再次調(diào)用可組合函數(shù)。這樣做會導(dǎo)致函數(shù)進(jìn)行重組 -- 系統(tǒng)會根據(jù)需要使用新數(shù)據(jù)重新繪制函數(shù)發(fā)出的微件。Compose 框架可以智能地僅重組已更改的組件。

例如,假設(shè)有以下可組合函數(shù),它用于顯示一個按鈕:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}

每次點擊該按鈕時,調(diào)用方都會更新 clicks 的值。Compose 會再次調(diào)用 lambda 與 Text 函數(shù)以顯示新值;此過程稱為“重組”。不依賴于該值的其他函數(shù)不會進(jìn)行重組。

如前文所述,重組整個界面樹在計算上成本高昂,因為會消耗計算能力并縮短電池續(xù)航時間。Compose 使用智能重組來解決此問題。

重組是指在輸入更改時再次調(diào)用可組合函數(shù)的過程。當(dāng)函數(shù)的輸入更改時,會發(fā)生這種情況。當(dāng) Compose 根據(jù)新輸入重組時,它僅調(diào)用可能已更改的函數(shù)或 lambda,而跳過其余函數(shù)或 lambda。通過跳過所有未更改參數(shù)的函數(shù)或 lambda,Compose 可以高效地重組。

切勿依賴于執(zhí)行可組合函數(shù)所產(chǎn)生的附帶效應(yīng),因為可能會跳過函數(shù)的重組。如果您這樣做,用戶可能會在您的應(yīng)用中遇到奇怪且不可預(yù)測的行為。附帶效應(yīng)是指對應(yīng)用的其余部分可見的任何更改。例如,以下操作全部都是危險的附帶效應(yīng):

  • 寫入共享對象的屬性
  • 更新 ViewModel 中的可觀察項
  • 更新共享偏好設(shè)置

PS: 這里更像U3D或者其他腳本周期的update周期函數(shù)刷新機(jī)制,不能隨便亂用。

可組合函數(shù)可能會像每一幀一樣頻繁地重新執(zhí)行,例如在呈現(xiàn)動畫時。可組合函數(shù)應(yīng)快速執(zhí)行,以避免在播放動畫期間出現(xiàn)卡頓。如果您需要執(zhí)行成本高昂的操作(例如從共享偏好設(shè)置讀取數(shù)據(jù)),請在后臺協(xié)程中執(zhí)行,并將值結(jié)果作為參數(shù)傳遞給可組合函數(shù)。

PS: 也就是說,把耗時的操作交給協(xié)程處理(理解成子線程)把最終結(jié)果交給作為參數(shù)給函數(shù),那么函數(shù)就會監(jiān)聽結(jié)果的變化動態(tài)更新。

例如,以下代碼會創(chuàng)建一個可組合項以更新 SharedPreferences 中的值。該可組合項不應(yīng)從共享偏好設(shè)置本身讀取或?qū)懭耄谑谴舜a將讀取和寫入操作移至后臺協(xié)程中的 ViewModel。應(yīng)用邏輯會使用回調(diào)傳遞當(dāng)前值以觸發(fā)更新。

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

在 Compose 中編程時,有許多事項需要注意:

  • 可組合函數(shù)可以按任何順序執(zhí)行。
  • 可組合函數(shù)可以并行執(zhí)行。
  • 重組會跳過盡可能多的可組合函數(shù)和 lambda。
  • 重組是樂觀的操作,可能會被取消。
  • 可組合函數(shù)可能會像動畫的每一幀一樣非常頻繁地運行。

如何構(gòu)建可組合函數(shù)以支持重組。在每種情況下,最佳做法都是使可組合函數(shù)保持快速、冪等且沒有附帶效應(yīng)。

可組合函數(shù)可以按任何順序執(zhí)行

Compose 可以通過并行運行可組合函數(shù)來優(yōu)化重組。這樣一來,Compose 就可以利用多個核心,并以較低的優(yōu)先級運行可組合函數(shù)(不在屏幕上)。

這種優(yōu)化意味著,可組合函數(shù)可能會在后臺線程池中執(zhí)行。如果某個可組合函數(shù)對 ViewModel 調(diào)用一個函數(shù),則 Compose 可能會同時從多個線程調(diào)用該函數(shù)。

為了確保應(yīng)用正常運行,所有可組合函數(shù)都不應(yīng)有附帶效應(yīng),而應(yīng)通過始終在界面線程上執(zhí)行的 onClick 等回調(diào)觸發(fā)附帶效應(yīng)。

調(diào)用某個可組合函數(shù)時,調(diào)用可能發(fā)生在與調(diào)用方不同的線程上。這意味著,應(yīng)避免使用修改可組合 lambda 中的變量的代碼,既因為此類代碼并非線程安全代碼,又因為它是可組合 lambda 不允許的附帶效應(yīng)。

以下示例展示了一個可組合項,它顯示一個列表及其項數(shù):

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

此代碼沒有附帶效應(yīng),它會將輸入列表轉(zhuǎn)換為界面。此代碼非常適合顯示小列表。不過,如果函數(shù)寫入局部變量,則這并非線程安全或正確的代碼:

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // Avoid! Side-effect of the column recomposing.
            }
        }
        Text("Count: $items")
    }
}

PS: 這個錯誤不安全代碼,在重組中賦值操作局部變量,那么導(dǎo)致計數(shù)錯誤出現(xiàn)。

重組會跳過盡可能多的內(nèi)容

如果界面的某些部分無效,Compose 會盡力只重組需要更新的部分。這意味著,它可以跳過某些內(nèi)容以重新運行單個按鈕的可組合項,而不執(zhí)行界面樹中在其上面或下面的任何可組合項。

每個可組合函數(shù)和 lambda 都可以自行重組。以下示例演示了在呈現(xiàn)列表時重組如何跳過某些元素:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.h5)
        Divider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

PS: 我的理解就是局部刷新,[header]更改時,它將重新組合,但[names]更改時,則不會重新組合

LazyColumn 是Compose的一個RecyclerView版本。
傳遞給items() 的lambda類似傳統(tǒng)RecyclerView的ViewHolder
當(dāng)items的name發(fā)生改變,只會重組item,不影響header,header也不會重組

這些作用域中的每一個都可能是在重組期間執(zhí)行的唯一一個作用域。當(dāng) header 發(fā)生更改時,Compose 可能會跳至 Column lambda,而不執(zhí)行它的任何父項。此外,執(zhí)行 Column 時,如果 names 未更改,Compose 可能會選擇跳過 LazyColumnItems。

同樣,執(zhí)行所有可組合函數(shù)或 lambda 都應(yīng)該沒有附帶效應(yīng)。當(dāng)您需要執(zhí)行附帶效應(yīng)時,應(yīng)通過回調(diào)觸發(fā)。

PS: 可以理解為原子操作,一個lambda只做一件事,沒有關(guān)聯(lián)。

重組是樂觀的操作

只要 Compose 認(rèn)為某個可組合項的參數(shù)可能已更改,就會開始重組。(可能就是發(fā)生改變)Compose 預(yù)計會在參數(shù)再次更改之前完成重組。如果某個參數(shù)在重組完成之前發(fā)生更改,Compose 可能會取消重組,并使用新參數(shù)重新開始。

PS:就是當(dāng)參數(shù)第一次更改,Compose監(jiān)聽到 第二次更改到來,那么會停止第一次刷新,使用第二次的新值進(jìn)行刷新,保持最新。

取消重組后,Compose 會從重組中舍棄界面樹。如有任何附帶效應(yīng)依賴于顯示的界面,則即使取消了組成操作,也會應(yīng)用該附帶效應(yīng)。這可能會導(dǎo)致應(yīng)用狀態(tài)不一致。
確保所有可組合函數(shù)和 lambda 都冪等且沒有附帶效應(yīng),以處理樂觀的重組。

可組合函數(shù)可能會非常頻繁地運行

在某些情況下,可能會針對界面動畫的每一幀運行一個可組合函數(shù)。如果該函數(shù)執(zhí)行成本高昂的操作(例如從設(shè)備存儲空間讀取數(shù)據(jù)),可能會導(dǎo)致界面卡頓。

例如,如果您的微件嘗試讀取設(shè)備設(shè)置,它可能會在一秒內(nèi)讀取這些設(shè)置數(shù)百次,這會對應(yīng)用的性能造成災(zāi)難性的影響。

如果您的可組合函數(shù)需要數(shù)據(jù),它應(yīng)為相應(yīng)的數(shù)據(jù)定義參數(shù)。然后,您可以將成本高昂的工作移至組成操作線程之外的其他線程,并使用 mutableStateOf 或 LiveData 將相應(yīng)的數(shù)據(jù)傳遞給 Compose。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,743評論 2 370

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