Android Kotlin Jetpack Compose UI框架 徹底解析

本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!php

(本篇涉及內容較多,篇幅較長,建議收藏,靜心閱讀。)html

前言

Q1的時候公司列了個培訓計劃,部分人做爲講師要上報培訓課題。那時候剛從好幾個Android項目裏抽離出來,正好看到Jetpack發佈了新玩意兒——Compose,我被它的快速實時打包給吸引住了,就準備調研一下,因而上報了這次課題。前端

但是計劃總趕不上變化,剛把課題報上去,我就扎入了前端的水深火熱之中。從0到1地學習前端,一邊學一邊作項目,一邊作項目一邊分享,思考怎麼讓別人也學會作前端項目,這段時間,真的酸爽。android

隨着時間推移,以前上報的課題分享也快臨近了,這纔想起來我還欠一個交代。沒辦法,本身報的課題,熬夜也要趕出來。git

下面,我會從這幾個方面來闡述此次課題:github

  • Compose是什麼
  • 如何優雅地使用Compose
  • 最後,Compose是否值得一試

名詞解析:編程

如下用到的專業術語可能會有出入,爲了不混淆,下面作一個名詞解析表:後端

名詞 解析 備註
組件 能夠控制頁面展現的部分UI的邏輯單元
View 能夠展現的UI,並具有本身維護狀態的能力
微件 組件,能夠控制頁面展現的部分UI的邏輯單元

Compose官方文檔中,新發明了一個名詞——「微件」 微件 能夠理解爲Android目前用到的各類 View,也能夠理解爲H5前端裏常說的 組件安全

1 Compose是什麼

Jetpack Compose 是用於構建原生界面的新款 Android 工具包。它可簡化並加快 Android 上的界面開發。使用更少的代碼、強大的工具和直觀的 Kotlin API,快速讓應用生動而精彩。markdown

這麼一聽感受有點抽象,不知道再講什麼。

我來翻譯一下:

Jetpack Compose 是一款基於Kotlin API,從新定義Android佈局的一套框架,它能夠更快速地實現Android原生應用。節省開發時長,減小包體積,提升應用性能。

節省開發時長,減小包體積,提升應用性能。 這個聽起來很誘人,咱們來看看它的效果如何。

1.1 Android Studio 對Compose 的支持

(本節要感謝 依然範特稀西 提供的實踐數據)

這一功能基於新版Android Studio 對Compose 的支持。

新版的Android Studio Arctic Fox(如今仍是Canary版本) 中添加了許多新工具來支持Jetpack Compose新特性,好比:實時文字、動畫預覽,佈局檢查等等。

1.1.1 強大的預覽

新的Android Studio 增長了對文字更改實時預覽的效果,能夠在Preview、模擬器、或者真機上實時預覽。

111.gif

1.1.2 動畫預覽

能夠在AndroidStudio內查看、檢查或播放動畫,還能夠逐針播放。

222.gif

1.1.3 佈局檢查器

Android Studio Arctic Fox 增長了佈局監測器對Compose的支持,能夠分析Compose組件的層級。以下所示:

333.gif

1.1.4 交互式預覽

在此模式下,你能夠與界面組件互動、點擊組件,以及查看狀態如何變化。經過這種方式,你能夠快速得到有關界面如何反應的反饋,並可快速預覽動畫。如要啓用此模式,只需點擊「互動」圖標 ,系統即會切換預覽模式。

89074-iw717wisn5b.png

如需中止此模式,請點擊頂部工具欄中的 Stop Interactive Preview。

以上是AndroidStudio對Compose的支持,能夠說是大手筆了。

1.2 Jetpack Compose 使用先後對比

你覺得Compose只是添加了預覽功能?那可不是。

從普通應用切換到Compose應用,你的應用速度和性能能夠獲得大幅提高。

咱們來看一個Google官方改造的應用示例。

1.2.1 APK 尺寸縮減

用戶最爲關心的指標,莫過於 APK 大小。

下面是開啓了 資源縮減 的最小化發佈版 APK (使用了 R8) 經過 APK Analyzer 所測量的結果:

63402-996raj8mnrr.png

97775-9mv5wydrwu8.png

關於上述數字的說明:

一、使用了 APK Analyzer 報告的 "APK file size" (而不是下載時的大小)。 APK 大小分析

二、在使用了 Compose 後,咱們發現 APK 大小縮減了 41%,方法數減小了 17%

1.2.2 代碼行數

源代碼行數雖然不能做爲衡量軟件好壞的標準,可是能夠對比出一個實驗在「瘦身」上面作了多大的努力,爲觀察實驗變化提供了一個統計視角。

78697-3u9txjsjll3.png

從圖中能夠看到,XML 行數大幅減小了 76%再見了,佈局文件,以及 styles、theme 等其餘的 XML 文件 。

同時,Kotlin 代碼的總行數也降低了。

這就是 APK 可以瘦身的很大一部分緣由。

1.2.3 構建速度

構建速度是開發者們十分關心的一項指標。

31273-ry24br610mn.png

這裏須要作一些說明:

"徹底接入 Compose" 使用的是最新版本的 Dagger/Hilt,該版本使用了 Android Gradle Plugin 7.0 中的新 ASM API。而其餘版本使用了較舊的 Hilt 版本,其使用了不一樣的機制,會嚴重拖慢生成 dex 文件的時間。

除此以外,Kotlin 編譯器與 Compose 編譯器插件爲咱們所作的事情,如 位置記憶化、細粒度重組 等工做,構建時間可以 減小 29%, 能夠說十分驚人。

2 如何優雅地使用Compose

上面講了不少Compose的優勢,那麼,接下來咱們如何使用它呢。

2.1 準備

在開始使用Compose以前,你須要具有一下基礎。

  • 下載 Android Studio Arctic Fox 或更高版本
  • Kotlin 1.4.32 或更高版本
  • Kotlin 語言使用無障礙

2.2 快速搭建Compose

如何快速搭建Compose新項目,以及如何將舊有項目遷移成Compose項目。

具體實踐步驟,我在**《用Android 最新UI 框架 「 Compose 」快速構建應用》** 裏已經講得很是詳細,這裏再也不贅述。

2.3 如何快速學習Compose

首先,恭喜你看到這裏,這篇文章看完,你就能夠上手開發了。

你也能夠到官網提供的 【快速上手】 示例教程 學習如何快速使用Compose基礎。

你還能夠在YouTube上觀看【教學視頻】

或者到gitHub下載【demo集合】,官方提供了不少demo示例,見下方示例圖:

98098-yv3otge0svl.png

2.4 Compose編程思想

瞭解瞭如何搭建,以及如何編寫demo,最終應用到項目以前,你還須要瞭解一些必備且重要的Compose基礎。

首當其衝的是 編程思想

2.4.1 聲明性編程範式

在Compose以前,咱們最多見的界面更新方式是使用 findViewById() 方法找到UI控件,並經過調用 button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap) 等方法更新控件。

這種手動操作佈局的方式,能夠提升代碼可讀性,但也容易出錯。好比:A控件被移除後緊接着在另外一段代碼中又給A佈局賦值。這可能會致使代碼異常。

在過去的幾年中,整個行業已開始轉向聲明性界面模型,該模型大大簡化了與構建和更新界面關聯的工程設計。該技術的工做原理是在概念上從頭開始從新生成整個屏幕,而後僅執行必要的更改。此方法可避免手動更新有狀態視圖層次結構的複雜性。

簡單點說,就是聲明性佈局能夠作到只更新須要更新的佈局,而不會由於一個小改動刷新整個屏幕,這是性能提高的一大進步。

Compose 是一個聲明性界面框架。

從新生成整個屏幕所面臨的一個難題是,在時間、計算能力和電池用量方面可能成本高昂。爲了減輕這一成本,**Compose 會智能地選擇在任何給定時間須要從新繪製界面的哪些部分。**這會對你設計界面組件的方式有必定影響,下面會說到。

2.4.2 簡單的可組合函數

使用 Compose,你能夠經過定義一組接受數據而發出界面元素的可組合函數來構建界面。

下面一個簡單的示例是 Greeting 組件,它接受 String 類型文案,而發出一個顯示問候消息的 Text 組件。

47696-71jthhgl94i.png

Greeting組件解析:

1.此函數帶有 @Composable 註釋。全部可組合函數都必須帶有此註釋;此註釋可告知 Compose 編譯器:此函數旨在將數據轉換爲界面。

2.微件接受一個 String,所以它能夠按名稱問候用戶。

3.此函數能夠在界面中顯示文本。爲此,它會調用 Text() 可組合函數,該函數實際上會建立文本界面元素。可組合函數經過調用其餘可組合函數來發出界面層次結構。

4.此函數不會返回任何內容。發出界面的 Compose 函數不須要返回任何內容,由於它們描述所需的屏幕狀態,而不是構造界面微件。

5.此函數快速、冪等且沒有反作用。

  • 5.1 使用同一參數屢次調用此函數時,它的行爲方式相同,而且它不使用其餘值,如全局變量或對 random() 的調用。
  • 5.2 此函數描述界面而沒有任何反作用,如修改屬性或全局變量。

2.4.3 聲明性範式轉變

在以往的 XML 佈局編程時,一般會經過增長 XML 佈局文件來實現佈局擴張,每一個 View 內部會維護本身狀態,而且提供 gettersetter 方法,容許邏輯與 View 進行交互。

在 Compose 的聲明性方法中, View 相對無狀態,而且不提供 setter 或 getter 函數。

實際上, View 不會以對象形式提供。

你能夠經過調用帶有不一樣參數的同一可組合函數來更新界面。這使得向架構模式(如 ViewModel)提供狀態變得很容易,如應用架構指南中所述。

而後,可組合項函數 負責在每次可觀察數據更新時將當前應用狀態轉換爲界面。

(下圖示例:一個數據源像下傳遞,應用到每一個佈局,須要刷新界面時,只須要刷新數據源) 66403-uw4wckawpp.png

(下圖示例:一個字佈局出發點擊事件時,事件向上傳遞,最後更改數據源,界面得以刷新) 88852-bnf1vohdnms.png

2.4.4 動態內容

因爲可組合函數是用 Kotlin 而不是 XML 編寫的,所以它們能夠像其餘任何 Kotlin 代碼同樣動態。例如,假設你想要構建一個界面,用來問候一些用戶

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

此函數接受名稱的列表,併爲每一個用戶生成一句問候語。

可組合函數可能很是複雜。你能夠根據功能,使用kotlin進行任意邏輯改造,全部這些動態切定製的內容,是 Compose對比傳統xml的優點。

2.4.5 重組

在命令式界面模型中(XML界面模型),如需更改某個View,你能夠在該View上調用 setter 以更改內部狀態。

Compose 中,你可使用新數據再次調用可組合函數。

這樣作會致使函數進行重組 -- 系統會根據須要使用新數據從新繪製函數發出的View。

Compose 框架能夠智能地僅重組已更改的組件。

例如,假設有如下可組合函數,它用於顯示一個按鈕:

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

以上代碼,每次點擊該按鈕時,調用方都會更新 clicks 的值。Compose 會再次調用 lambdaText 函數以顯示新值;此過程稱爲「重組」。不依賴於該值的其餘函數不會進行重組。

重組整個界面樹在計算上成本高昂,由於會消耗計算能力並縮短電池續航時間。

Compose 根據新輸入重組時,它僅調用可能已更改的函數或 lambda,而跳過其他函數或 lambda。經過跳過全部未更改參數的函數或 lambdaCompose 能夠高效地重組

切勿依賴於執行可組合函數所產生的附帶效應,由於可能會跳過函數的重組。

附帶效應:是指對應用的其他部分可見的任何更改。

例如,如下操做所有都是危險的附帶效應:

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

舉個例子:

如下代碼會建立一個可組合項以更新 SharedPreferences 中的值。

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

該可組合項不該從共享偏好設置自己讀取或寫入,因而此代碼將讀取和寫入操做移至後臺協程中的 ViewModel。應用邏輯會使用回調傳遞當前值以觸發更新。

2.4.6 使用Compose的注意事項

如下是在 Compose 中編程時須要注意的事項:

  • 可組合函數能夠按任何順序執行。
  • 可組合函數能夠並行執行。
  • 重組會跳過儘量多的可組合函數和 lambda。
  • 重組是樂觀的操做,可能會被取消。
  • 可組合函數可能會像動畫的每一幀同樣很是頻繁地運行。

在每種狀況下,最佳作法都是使可組合函數保持快速、冪等且沒有附帶效應。

可組合函數能夠按任何順序執行。

例如,假設有以下代碼,用於在標籤頁佈局中繪製三個屏幕:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}
複製代碼

StartScreenMiddleScreenEndScreen 的調用能夠按任何順序進行。這意味着,舉例來講,你不能讓 StartScreen() 設置某個全局變量(附帶效應)並讓 MiddleScreen() 利用這項更改。相反,其中每一個函數都須要保持獨立。

可組合函數能夠並行執行。

Compose 能夠經過並行運行可組合函數來優化重組。 這樣一來,Compose 就能夠利用多個核心,並以較低的優先級運行可組合函數(不在屏幕上)。

這種優化意味着,可組合函數可能會在後臺線程池中執行。

假設多個可組合函數調用了 ViewModel 裏的方法A,那麼方法A會被多個線程調用,須要作好線程同步工做。

也由於可並行執行的特色,調用可能發生在與調用方不一樣的線程上。所以,全部可組合函數都不該有附帶效應(好比修改一個全局變量),而應經過始終在界面線程上執行的 onClick 等回調觸發附帶效應。

如下示例展現了一個可組合項,它顯示一個列表及其項數:

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

此代碼沒有附帶效應,它會將輸入列表轉換爲界面。此代碼很是適合顯示小列表。不過,若是函數寫入局部變量,則這並不是線程安全或正確的代碼:

@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")
    }
}
複製代碼

在本例中,每次重組時,都會修改 items。這能夠是動畫的每一幀,或是在列表更新時。但無論怎樣,界面都會顯示錯誤的項數。所以,Compose 不支持這樣的寫入操做;經過禁止此類寫入操做,咱們容許框架更改線程以執行可組合 lambda。

重組會跳過儘量多的可組合函數和 lambda。

若是界面的某些部分無效,Compose 會盡力只重組須要更新的部分。這意味着,它能夠跳過某些內容以從新運行單個按鈕的可組合項,而不執行界面樹中在其上面或下面的任何可組合項。

每一個可組合函數和 lambda 均可以自行重組。如下示例演示了在呈現列表時重組如何跳過某些元素:

/**
 * 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) }))
}
複製代碼

這些做用域中的每個均可能是在重組期間執行的惟一一個做用域。當 header 發生更改時,Compose 可能會跳至 Column lambda,而不執行它的任何父項。此外,執行 Column 時,若是 names 未更改,Compose 可能會選擇跳過 LazyColumnItems

執行全部可組合函數或 lambda 都應該沒有附帶效應。當你須要執行附帶效應時,應經過回調觸發。

重組是樂觀的操做,可能會被取消。

重組是樂觀的操做,也就是說,Compose 預計會在參數再次更改以前完成重組。若是某個參數在重組完成以前發生更改,Compose 可能會取消重組,並使用新參數從新開始。

取消重組後,Compose 會從重組中捨棄界面樹。

若有任何附帶效應依賴於顯示的界面,則即便取消了組成操做,也會應用該附帶效應。這可能會致使應用狀態不一致(致使狀態錯亂,或重複賦值)。

可組合函數可能會像動畫的每一幀同樣很是頻繁地運行。

在某些狀況下,可能會針對界面動畫的每一幀運行一個可組合函數。若是該函數執行成本高昂的操做(例如從設備存儲空間讀取數據),可能會致使界面卡頓。

若是可組合函數須要數據,它應爲相應的數據定義參數,從參數中獲取。

你能夠將成本高昂的工做移至組成操做線程以外的其餘線程,並使用 mutableStateOfLiveData 將相應的數據傳遞給 Compose

**總結:**可組合函數應儘可能寫成純函數,數據僅從參數中獲取,更改數據僅從用戶操做事件中進行。全部異步數據須要先準備好,再傳入函數的參數中。

3 Compose是否值得一試

前面講到Compose的特性,優缺點,以及如何快速入門、如何正確使用。

那麼Compose是否值得應用到項目中來呢?

這些還須要具體狀況具體分析。

若是你是新項目

我建議你大膽嚐鮮,畢竟聰明的「部分刷新」機制,是提升頁面性能的重要手段。並且聲明式佈局在將來應該會取代傳統的xml佈局形式,這是大勢所趨。

若是你是現有項目改造。

首先,你能夠評估一下是否已經具有開始Compose的基礎能力——kotlin語言的靈活運用

Compose能夠說是爲Kotlin量身定製的、與View model緊密結合的一種衍生物,有了KotlinView modelCompose的做用能夠發揮到極致,也就能實現前面的目標:

  • 構建時間可以 減小 29%
  • XML 行數大幅減小了 76%
  • APK 大小縮減了 41%
  • 方法數減小了 17%

若是你已經具有了上述能力,那麼能夠在小範圍進行試點,或者從性能要求比較高的頁面入手。

建議先單個頁面引入,最後再作全量替換。Google官方的改造案例也是這麼作的。

最後,放開手,擼起來吧!

社區須要你我共建,更須要走在前沿的實踐者,期待看到更多、更好的文章出現,這就是我寫做的動力。

相關文章
相關標籤/搜索