什么是Android Jetpack
Android Jetpack是一個由多個庫組成的套件,可幫助開發者遵循最佳做法、減少樣板代碼並編寫可在各種Android版本和設備中一致運行的代碼,讓開發者可將精力集中於真正重要的編碼工作。
什么是Jetpack Compose
Jetpack Compose是用於構建原生Android界面的新工具包。它可簡化並加快Android上的界面開發,幫助您使用更少的代碼、強大的工具和直觀的Kotlin API,快速打造生動而精彩的應用。
Jetpack Compose
是用於構建原生界面的最新的Android工具包,采用聲明式UI的設計,擁有更簡單的自定義和實時的交互預覽功能,由Android官方團隊全新打造的UI框架。
為什么采用Compose
Jetpack Compose是用於構建原生Android界面的新工具包。它使用更少的代碼、強大的工具和直觀的Kotlin API,可以幫助您簡化並加快Android界面開發,打造生動而精彩的應用。它可讓您更快速、更輕松地構建Android界面。
更少的代碼
編寫更少的代碼會影響到所有開發階段:作為代碼撰寫者,需要測試和調試的代碼會更少,出現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
將Android Studio與Jetpack Compose配合使用
為了在使用Jetpack Compose進行開發時獲得最佳體驗,您應下載最新版本的Android Studio Arctic Fox。這是因為,當您搭配使用Android Studio和Jetpack Compose開發應用時,可以從智能編輯器功能中受益,這些功能包括“新建項目”模板和即時預覽Compose界面等。
安裝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 的新項目,請按以下步驟操作:
- 如果您位於 Welcome to Android Studio 窗口中,請點擊 Start a new Android Studio project。如果您已打開 Android Studio 項目,請從菜單欄中依次選擇 File > New > New Project。
- 在 Select a Project Template 窗口中,選擇 Empty Compose Activity,然后點擊 Next。
- 在 Configure your project 窗口中,執行以下操作:
- 按照常規方法設置 Name、Package name 和 Save location。
- 請注意,在 Language 下拉菜單中,Kotlin 是唯一可用的選項,因為 Jetpack Compose 僅適用於使用 Kotlin 編寫的類。
- 在 Minimum API level dropdown 菜單中,選擇 API 級別 21 或更高級別。
- 點擊 Finish。
- 根據配置 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
微件。
顯示文本“HelloWorld”的手機的屏幕截圖,以及用於生成該界面的簡單可組合函數的代碼
圖1.一個簡單的可組合函數,系統向它傳遞了數據,它使用該數據在屏幕上呈現文本微件。
關於此函數,有幾點值得注意:
-
此函數帶有
@Composable
注釋。所有可組合函數都必須帶有此注釋;此注釋可告知Compose編譯器:此函數旨在將數據轉換為界面。 -
此函數接受數據。可組合函數可以接受一些參數,這些參數可讓應用邏輯描述界面。在本例中,我們的微件接受一個
String
,因此它可以按名稱問候用戶。 -
此函數可以在界面中顯示文本。為此,它會調用
Text()
可組合函數,該函數實際上會創建文本界面元素。可組合函數通過調用其他可組合函數來發出界面層次結構。 -
此函數不會返回任何內容。發出界面的Compose函數不需要返回任何內容,因為它們描述所需的屏幕狀態,而不是構造界面微件。
-
此函數快速、冪等且沒有副作用。
- 使用同一參數多次調用此函數時,它的行為方式相同,並且它不使用其他值,如全局變量或對random()的調用。
- 此函數描述界面而沒有任何副作用,如修改屬性或全局變量。
- 一般來說,出於重組部分所述的原因,所有可組合函數都應使用這些屬性來編寫。
聲明性范式轉變
在許多面向對象的命令式界面工具包中,您可以通過實例化微件樹來初始化界面。您通常通過膨脹XML布局文件來實現此目的。每個微件都維護自己的內部狀態,並且提供getter和setter方法,允許應用邏輯與微件進行交互。
在Compose的聲明性方法中,微件相對無狀態,並且不提供setter或getter函數。實際上,微件不會以對象形式提供。您可以通過調用帶有不同參數的同一可組合函數來更新界面。這使得向架構模式(如ViewModel
)提供狀態變得很容易,如應用架構指南中所述。然后,可組合項負責在每次可觀察數據更新時將當前應用狀態轉換為界面。
圖2.應用邏輯為頂級可組合函數提供數據。該函數通過調用其他可組合函數來使用這些數據描述界面,將適當的數據傳遞給這些可組合函數,並沿層次結構向下傳遞數據。
當用戶與界面交互時,界面會發起onClick
等事件。這些事件應通知應用邏輯,應用邏輯隨后可以改變應用的狀態。當狀態發生變化時,系統會使用新數據再次調用可組合函數。這會導致重新繪制界面元素,此過程稱為“重組”。
說明界面元素如何通過觸發由應用邏輯處理的事件來響應交互的圖示。
圖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。