乘風破浪,遇見Android Jetpack之Compose宣告式UI開發工具包,逐漸大一統的原生UI繪製體系

TaylorShi發表於2021-08-12

什麼是Android Jetpack

https://developer.android.com/jetpack

image

Android Jetpack是一個由多個庫組成的套件,可幫助開發者遵循最佳做法、減少樣板程式碼並編寫可在各種Android版本和裝置中一致執行的程式碼,讓開發者可將精力集中於真正重要的編碼工作。

什麼是Jetpack Compose

Jetpack Compose是用於構建原生Android介面的新工具包。它可簡化並加快Android上的介面開發,幫助您使用更少的程式碼、強大的工具和直觀的Kotlin API,快速打造生動而精彩的應用。

https://developer.android.com/jetpack/compose

image

Jetpack Compose是用於構建原生介面的最新的Android工具包,採用宣告式UI的設計,擁有更簡單的自定義和實時的互動預覽功能,由Android官方團隊全新打造的UI框架。

image

為什麼採用Compose

Jetpack Compose是用於構建原生Android介面的新工具包。它使用更少的程式碼、強大的工具和直觀的Kotlin API,可以幫助您簡化並加快Android介面開發,打造生動而精彩的應用。它可讓您更快速、更輕鬆地構建Android介面。

image

更少的程式碼

編寫更少的程式碼會影響到所有開發階段:作為程式碼撰寫者,需要測試和除錯的程式碼會更少,出現bug的可能性也更小,您就可以專注於解決手頭的問題;作為稽核人員或維護人員,您需要閱讀、理解、稽核和維護的程式碼就更少。

與使用AndroidView系統(按鈕、列表或動畫)相比,Compose可讓您使用更少的程式碼實現更多的功能。無論您需要構建什麼內容,現在需要編寫的程式碼都更少了。以下是我們的一些合作伙伴的感想:

  • “對於相同的Button類,程式碼的體量要小10倍。”(Twitter)
  • “使用RecyclerView構建的任何螢幕(我們的大部分螢幕都使用它構建)的大小也顯著減小。”(Monzo)
  • ““只需要很少幾行程式碼就可以在應用中建立列表或動畫,這一點令我們非常滿意。對於每項功能,我們編寫的程式碼行更少了,這讓我們能夠將更多精力放在為客戶提供價值上。”(Cuvva)
  • 編寫程式碼只需要採用Kotlin,而不必拆分成Kotlin和XML部分:“當所有程式碼都使用同一種語言編寫並且通常位於同一檔案中(而不是在Kotlin和XML語言之間來回切換)時,跟蹤變得更容易”(Monzo)

無論您要構建什麼,使用Compose編寫的程式碼都很簡潔且易於維護。“Compose的佈局系統在概念上更簡單,因此可以更輕鬆地推斷。檢視複雜元件的程式碼也更輕鬆。”(Square)

直觀

Compose使用宣告性API,這意味著您只需描述介面,Compose會負責完成其餘工作。這類API十分直觀-易於探索和使用:“我們的主題層的直觀和清晰程度顯著提高。我們能夠在單個Kotlin檔案中完成之前需要在多個XML檔案中完成的任務,這些XML檔案負責通過多個分層主題疊加層定義和分配屬性。”(Twitter)

利用Compose,您可以構建不與特定activity或fragment相關聯的小型無狀態元件。這讓您可以輕鬆重用和測試這些元件:“我們給自己設定的目標是,交付一組新的無狀態介面元件,確保它們易於使用和維護,且可直觀實現/擴充套件/自定義。就這一點而言,Compose確實為我們提供了一個可靠的答案。”(Twitter)

在Compose中,狀態是顯式的,並且會傳遞給相應的可組合項。這樣一來,狀態便具有單一可信來源,因而是封裝和分離的。然後,應用狀態變化時,介面會自動更新。“在對某些內容進行推斷時,不必處理太多資訊,並且無法控制或難以理解的行為也更少”(Cuvva)

加快應用開發

Compose與您所有的現有程式碼相容:您可以從View呼叫Compose程式碼,也可以從Compose呼叫View。大多數常用庫(如Navigation、ViewModel和Kotlin協程)都適用於Compose,因此您可以隨時隨地開始採用。“我們一開始整合Compose是為了實現互操作性,並且這樣確實‘行之有效’。我們發現,我們不必考慮淺色模式和深色模式等問題,整個體驗無比順暢。”(Cuvva)

藉助全面的AndroidStudio支援以及實時預覽等功能,您可以更快地迭代和交付程式碼:“AndroidStudio中的預覽功能極大地節省了我們的時間。能夠構建多個預覽也幫我們節省了時間。我們通常需要檢查不同狀態下或採用不同設定的介面元件(例如錯誤狀態或採用不同的字型大小等)。由於能夠建立多個預覽,我們可以輕鬆執行這些檢查。”(Square)

功能強大

利用Compose,您可以憑藉對Android平臺API的直接訪問和對於MaterialDesign、深色主題、動畫等的內建支援,建立精美的應用:“Compose不僅解決了宣告性介面的問題,還改進了無障礙功能API、佈局等各種內容。將設想變為現實所需的步驟更少了”(Square)。

利用Compose,您可以輕鬆快速地通過動畫讓應用變得生動有趣:“在Compose中新增動畫非常簡單,沒有理由不去為顏色/大小/高度變化新增動畫效果”(Monzo),“不需要任何特殊的工具就能製作動畫,這與顯示靜態螢幕沒有什麼不同”(Square)。

無論您是使用MaterialDesign還是自己的設計系統進行構建,Compose都可以讓您靈活地實現所需的設計:“從基礎上將MaterialDesign分離出來對我們來說非常有用,因為我們要構建自己的設計系統,這往往需要與Material不同的設計要求。”(Square)

使用入門

Jetpack Compose教程

https://developer.android.com/jetpack/compose/tutorial

image

將Android Studio與Jetpack Compose配合使用

為了在使用Jetpack Compose進行開發時獲得最佳體驗,您應下載最新版本的Android Studio Arctic Fox。這是因為,當您搭配使用Android Studio和Jetpack Compose開發應用時,可以從智慧編輯器功能中受益,這些功能包括“新建專案”模板和即時預覽Compose介面等。

下載Android Studio Arctic Fox

安裝Android Studio後,請按照以下說明嘗試Jetpack Compose示例應用,建立新的Jetpack Compose應用專案,或者向現有應用專案新增對Jetpack Compose的支援。

Jetpack Compose示例

https://github.com/android/compose-samples

如若要執行示例,至少需要使用Android Studio Arctic Fox

示例匯入Android Studio指南:https://developer.android.com/jetpack/compose/setup#sample

建立支援Jetpack Compose的新應用

https://developer.android.com/jetpack/compose/setup#create-new

如果您想要建立一個預設支援 Jetpack Compose 的新專案,Android Studio 提供了新專案模板來幫助您入手。如需建立支援 Jetpack Compose 的新專案,請按以下步驟操作:

  1. 如果您位於 Welcome to Android Studio 視窗中,請點選 Start a new Android Studio project。如果您已開啟 Android Studio 專案,請從選單欄中依次選擇 File > New > New Project。
  2. 在 Select a Project Template 視窗中,選擇 Empty Compose Activity,然後點選 Next。
  3. 在 Configure your project 視窗中,執行以下操作:
    • 按照常規方法設定 Name、Package name 和 Save location。
    • 請注意,在 Language 下拉選單中,Kotlin 是唯一可用的選項,因為 Jetpack Compose 僅適用於使用 Kotlin 編寫的類。
    • 在 Minimum API level dropdown 選單中,選擇 API 級別 21 或更高階別。
  4. 點選 Finish。
  5. 根據配置 Gradle 中所述的方法,驗證專案的 build.gradle 檔案配置是否正確。

將Jetpack Compose新增到現有專案中

https://developer.android.com/jetpack/compose/setup#add-compose

程式設計思想

Jetpack Compose是一個適用於Android的新式宣告性介面工具包。Compose提供宣告性API,讓您可在不以命令方式改變前端檢視的情況下呈現應用介面,從而使編寫和維護應用介面變得更加容易。此術語需要一些解釋說明,它的含義對應用設計非常重要。

宣告性程式設計正規化

長期以來,Android檢視層次結構一直可以表示為介面微件樹。由於應用的狀態會因使用者互動等因素而發生變化,因此介面層次結構需要進行更新以顯示當前資料。最常見的介面更新方式是使用findViewById()等函式遍歷樹,並通過呼叫button.setText(String)container.addChild(View)img.setImageBitmap(Bitmap)等方法更改節點。這些方法會改變微件的內部狀態。

手動操縱檢視會提高出錯的可能性。如果一條資料在多個位置呈現,很容易忘記更新顯示它的某個檢視。此外,當兩項更新以意外的方式發生衝突時,也很容易造成異常狀態。例如,某項更新可能會嘗試設定剛剛從介面中移除的節點的值。一般來說,軟體維護複雜性會隨著需要更新的檢視數量而增長。

在過去的幾年中,整個行業已開始轉向宣告性介面模型,該模型大大簡化了與構建和更新介面關聯的工程設計。該技術的工作原理是在概念上從頭開始重新生成整個螢幕,然後僅執行必要的更改。此方法可避免手動更新有狀態檢視層次結構的複雜性。Compose是一個宣告性介面框架。

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

簡單的可組合函式

使用Compose,您可以通過定義一組接受資料而發出介面元素的可組合函式來構建介面。一個簡單的示例是Greeting微件,它接受String而發出一個顯示問候訊息的Text微件。

image

顯示文字“HelloWorld”的手機的螢幕截圖,以及用於生成該介面的簡單可組合函式的程式碼

圖1.一個簡單的可組合函式,系統向它傳遞了資料,它使用該資料在螢幕上呈現文字微件。

關於此函式,有幾點值得注意:

  • 此函式帶有@Composable註釋。所有可組合函式都必須帶有此註釋;此註釋可告知Compose編譯器:此函式旨在將資料轉換為介面。

  • 此函式接受資料。可組合函式可以接受一些引數,這些引數可讓應用邏輯描述介面。在本例中,我們的微件接受一個String,因此它可以按名稱問候使用者。

  • 此函式可以在介面中顯示文字。為此,它會呼叫Text()可組合函式,該函式實際上會建立文字介面元素。可組合函式通過呼叫其他可組合函式來發出介面層次結構。

  • 此函式不會返回任何內容。發出介面的Compose函式不需要返回任何內容,因為它們描述所需的螢幕狀態,而不是構造介面微件。

  • 此函式快速、冪等且沒有副作用。

    • 使用同一引數多次呼叫此函式時,它的行為方式相同,並且它不使用其他值,如全域性變數或對random()的呼叫。
    • 此函式描述介面而沒有任何副作用,如修改屬性或全域性變數。
    • 一般來說,出於重組部分所述的原因,所有可組合函式都應使用這些屬性來編寫。

宣告性正規化轉變

在許多物件導向的命令式介面工具包中,您可以通過例項化微件樹來初始化介面。您通常通過膨脹XML佈局檔案來實現此目的。每個微件都維護自己的內部狀態,並且提供getter和setter方法,允許應用邏輯與微件進行互動。

在Compose的宣告性方法中,微件相對無狀態,並且不提供setter或getter函式。實際上,微件不會以物件形式提供。您可以通過呼叫帶有不同引數的同一可組合函式來更新介面。這使得向架構模式(如ViewModel)提供狀態變得很容易,如應用架構指南中所述。然後,可組合項負責在每次可觀察資料更新時將當前應用狀態轉換為介面。

image

圖2.應用邏輯為頂級可組合函式提供資料。該函式通過呼叫其他可組合函式來使用這些資料描述介面,將適當的資料傳遞給這些可組合函式,並沿層次結構向下傳遞資料。

當使用者與介面互動時,介面會發起onClick等事件。這些事件應通知應用邏輯,應用邏輯隨後可以改變應用的狀態。當狀態發生變化時,系統會使用新資料再次呼叫可組合函式。這會導致重新繪製介面元素,此過程稱為“重組”。

image

說明介面元素如何通過觸發由應用邏輯處理的事件來響應互動的圖示。

圖3.使用者與介面元素進行了互動,導致觸發一個事件。應用邏輯響應該事件,然後系統根據需要使用新引數自動再次呼叫可組合函式。

動態內容

由於可組合函式是用Kotlin而不是XML編寫的,因此它們可以像其他任何Kotlin程式碼一樣動態。例如,假設您想要構建一個介面,用來問候一些使用者:

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

此函式接受名稱的列表,併為每個使用者生成一句問候語。可組合函式可能非常複雜。您可以使用if語句來確定是否要顯示特定的介面元素。您可以使用迴圈。您可以呼叫輔助函式。您擁有底層語言的全部靈活性。這種強大的功能和靈活性是Jetpack Compose的主要優勢之一。

重組

在命令式介面模型中,如需更改某個微件,您可以在該微件上呼叫setter以更改其內部狀態。在Compose中,您可以使用新資料再次呼叫可組合函式。這樣做會導致函式進行重組--系統會根據需要使用新資料重新繪製函式發出的微件。Compose框架可以智慧地僅重組已更改的元件。

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

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

每次點選該按鈕時,呼叫方都會更新clicks的值。Compose會再次呼叫lambda與Text函式以顯示新值;此過程稱為“重組”。不依賴於該值的其他函式不會進行重組。

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

重組是指在輸入更改時再次呼叫可組合函式的過程。當函式的輸入更改時,會發生這種情況。當Compose根據新輸入重組時,它僅呼叫可能已更改的函式或lambda,而跳過其餘函式或lambda。通過跳過所有未更改引數的函式或lambda,Compose可以高效地重組。

切勿依賴於執行可組合函式所產生的附帶效應,因為可能會跳過函式的重組。如果您這樣做,使用者可能會在您的應用中遇到奇怪且不可預測的行為。附帶效應是指對應用的其餘部分可見的任何更改。例如,以下操作全部都是危險的附帶效應:

  • 寫入共享物件的屬性
  • 更新ViewModel中的可觀察項
  • 更新共享偏好設定

可組合函式可能會像每一幀一樣頻繁地重新執行,例如在呈現動畫時。可組合函式應快速執行,以避免在播放動畫期間出現卡頓。如果您需要執行成本高昂的操作(例如從共享偏好設定讀取資料),請在後臺協程中執行,並將值結果作為引數傳遞給可組合函式。

例如,以下程式碼會建立一個可組合項以更新SharedPreferences中的值。該可組合項不應從共享偏好設定本身讀取或寫入,於是此程式碼將讀取和寫入操作移至後臺協程中的ViewModel。應用邏輯會使用回撥傳遞當前值以觸發更新。

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

可組合函式可以按任何順序執行

如果您看一下可組合函式的程式碼,可能會認為這些程式碼按其出現的順序執行。但其實未必是這樣。如果某個可組合函式包含對其他可組合函式的呼叫,這些函式可以按任何順序執行。Compose可以選擇識別出某些介面元素的優先順序高於其他介面元素,因而首先繪製這些元素。

例如,假設您有如下程式碼,用於在標籤頁佈局中繪製三個螢幕:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

對StartScreen、MiddleScreen和EndScreen的呼叫可以按任何順序進行。這意味著,舉例來說,您不能讓StartScreen()設定某個全域性變數(附帶效應)並讓MiddleScreen()利用這項更改。相反,其中每個函式都需要保持獨立。

可組合函式可以並行執行

Compose可以通過並行執行可組合函式來優化重組。這樣一來,Compose就可以利用多個核心,並以較低的優先順序執行可組合函式(不在螢幕上)。

這種優化意味著,可組合函式可能會在後臺執行緒池中執行。如果某個可組合函式對ViewModel呼叫一個函式,則Compose可能會同時從多個執行緒呼叫該函式。

為了確保應用正常執行,所有可組合函式都不應有附帶效應,而應通過始終在介面執行緒上執行的onClick等回撥觸發附帶效應。

呼叫某個可組合函式時,呼叫可能發生在與呼叫方不同的執行緒上。這意味著,應避免使用修改可組合lambda中的變數的程式碼,既因為此類程式碼並非執行緒安全程式碼,又因為它是可組合lambda不允許的附帶效應。

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

@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。

重組會跳過儘可能多的內容

如果介面的某些部分無效,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可能會跳至Columnlambda,而不執行它的任何父項。此外,執行Column時,如果names未更改,Compose可能會選擇跳過LazyColumnItems。

同樣,執行所有可組合函式或lambda都應該沒有附帶效應。當您需要執行附帶效應時,應通過回撥觸發。

重組是樂觀的操作

只要Compose認為某個可組合項的引數可能已更改,就會開始重組。重組是樂觀的操作,也就是說,Compose預計會在引數再次更改之前完成重組。如果某個引數在重組完成之前發生更改,Compose可能會取消重組,並使用新引數重新開始。

取消重組後,Compose會從重組中捨棄介面樹。如有任何附帶效應依賴於顯示的介面,則即使取消了組成操作,也會應用該附帶效應。這可能會導致應用狀態不一致。

確保所有可組合函式和lambda都冪等且沒有附帶效應,以處理樂觀的重組。

可組合函式可能會非常頻繁地執行

在某些情況下,可能會針對介面動畫的每一幀執行一個可組合函式。如果該函式執行成本高昂的操作(例如從裝置儲存空間讀取資料),可能會導致介面卡頓。

例如,如果您的微件嘗試讀取裝置設定,它可能會在一秒內讀取這些設定數百次,這會對應用的效能造成災難性的影響。

如果您的可組合函式需要資料,它應為相應的資料定義引數。然後,您可以將成本高昂的工作移至組成操作執行緒之外的其他執行緒,並使用mutableStateOf或LiveData將相應的資料傳遞給Compose。

參考

相關文章