Android前沿技術—— Jetpack Compose

語言: CN / TW / HK

theme: channing-cyan

簡介

Jetpack Compose 是一款新型工具包,旨在幫助簡化界面開發。該工具包將響應式編程模型與簡潔易用的 Kotlin 編程語言相結合,並採用完全聲明式的代碼編寫方式,讓您可以通過調用一系列函數來描述界面,這些函數會將數據轉換為界面層次結構。當底層數據發生變化時,框架會自動重新執行這些函數,為您更新界面層次結構。

Compose 應用由可組合函數構成。可組合函數即帶有 @Composable 標記的常規函數,這些函數可以調用其他可組合函數。使用一個函數就可以創建一個新的界面組件。該註解會告知 Compose 為函數添加特殊支持,以便後續更新和維護界面。藉助 Compose,您可以將代碼設計成多個小代碼塊。可組合函數通常簡稱為“可組合項”。

通過創建可重用的小型可組合項,您可以輕鬆構建應用中所用界面元素的庫。每個可組合項對應於屏幕的一個部分,可以單獨修改。

Compose 使用入門

瞭解 Android Studio 為您生成的與 Compose 相關的各種類和方法。

可組合函數

可組合函數是帶有 @Composable 註解的常規函數。這類函數自身可以調用其他 @Composable 函數。我們會展示如何為 Greeting 函數添加 @Composable 標記。此函數會生成一段顯示給定輸入 String 的界面層次結構。Text 是由庫提供的可組合函數。

@Composableprivate fun Greeting(name: String) {   Text(text = "Hello $name!")}

注意:可組合函數是帶有 @Composable 註解的 Kotlin 函數,如上述代碼段所示。

Android 應用中的 Compose

使用 Compose 時,Activity 仍然是 Android 應用的入口點。在我們的項目中,用户打開應用時會啟動 MainActivity(如 AndroidManifest.xml 文件中所指定)。您可以使用 setContent 來定義佈局,但不同於在傳統 View 系統中使用 XML 文件,您將在該函數中調用可組合函數。

class MainActivity : AppCompatActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContent {           BasicsCodelabTheme {               // A surface container using the 'background' color from the theme               Surface(                 modifier = Modifier.fillMaxSize(),                 color = MaterialTheme.colors.background               ) {                   Greeting("Android")               }           }       }   } }

BasicsCodelabTheme 是為可組合函數設置樣式的一種方式。有關詳細內容,請參閲設置應用主題部分。如需查看文本在屏幕上的顯示效果,您可以在模擬器或設備上運行該應用,或使用 Android Studio 預覽進行查看。

若要使用 Android Studio 預覽,您只需使用 @Preview 註解標記所有無參數可組合函數或採用默認參數的函數,然後構建您的項目即可。現在 MainActivity.kt 文件中已經包含了一個 Preview Composable 函數。您可以在同一個文件中包含多個預覽,併為它們指定名稱。

@Preview(showBackground = true, name = "Text preview") @Composable fun DefaultPreview() {   BasicsCodelabTheme {       Greeting(name = "Android")   } }

debde226026ae047.png

注意:在此項目中導入與 Jetpack Compose 相關的類時,請從以下位置導入:

  • androidx.compose.*(針對編譯器和運行時類)
  • androidx.compose.ui.*(針對界面工具包和庫)

如果選擇 Code f66a8adcef249de5.png,系統可能不會顯示預覽。請點擊 Split f3c0e2f3221dadcb.png 以查看預覽。

調整界面

首先,為 Greeting 設置不同的背景顏色。為此,您可以用 Surface 包圍 Text 可組合項。Surface 會採用一種顏色,因此請使用 MaterialTheme.colors.primary

注意:SurfaceMaterialTheme 是與 Material Design 相關的概念。Material Design 是 Google 提供的一個設計系統,旨在幫助您構建界面和體驗。

@Composable private fun Greeting(name: String) {   Surface(color = MaterialTheme.colors.primary) {       Text (text = "Hello $name!")   } }

嵌套在 Surface 內的組件將在該背景顏色之上繪製。

將上述代碼添加到項目後,您會在 Android Studio 的右上角看到 Build & Refresh 按鈕。點按該按鈕或構建項目即可在預覽中查看新更改。

9632f3ca76cbe115.png

您可以在預覽中查看新更改:

8216bdbc85a6ba94.png

您可能忽略了一個重要的細節:文字現在是白色的。我們是何時對此進行定義的?

我們並沒有對此進行過定義!Material 組件(例如 androidx.compose.material.Surface)旨在提供應用中可能需要的常見功能(例如為文本選擇適當的顏色),讓您獲得更好的體驗。我們之所以説 Material 很實用,是因為它提供在大多數應用中都會用到的實用默認值和模式。Compose 中的 Material 組件是在其他基礎組件(位於 androidx.compose.foundation 中)的基礎上構建的。如果您需要更高的靈活性,也可以從您的應用組件中訪問這些組件。

在這種情況下,Surface 會了解,當該背景設置為 primary 顏色後,其上的任何文本都應使用 onPrimary 顏色,此顏色也在主題中進行了定義。如需瞭解詳情,請參閲設置應用主題部分。

注意:如需查看 Compose 中 Material 組件的交互式列表,請查看 Compose Material Catalog 應用。

修飾符

大多數 Compose 界面元素(例如 SurfaceText)都接受可選的 modifier 參數。修飾符會指示界面元素如何在其父級佈局中放置、顯示或表現。

例如,padding 修飾符會在其修飾的元素周圍應用一定的空間。您可以使用 Modifier.padding() 創建內邊距修飾符。

現在,為屏幕上的 Text 添加內邊距:

import androidx.compose.foundation.layout.padding import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp ... ​ @Composable private fun Greeting(name: String) {   Surface(color = MaterialTheme.colors.primary) {       Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))   } }

點擊 Build & Refresh 即可查看新更改。

4241a60d72a08f0b.png

有數十種修飾符可用於實現對齊、添加動畫、設置佈局、使可點擊或可滾動以及轉換等效果。有關完整列表,請查看 Compose 修飾符列表。您將在後續步驟中使用其中的部分修飾符。

重複使用可組合項

您添加到界面的組件越多,創建的嵌套層級就越多。如果函數變得非常大,可能會影響可讀性。通過創建可重用的小型組件,可以輕鬆構建應用中所用界面元素的庫。每個組件對應於屏幕的一個部分,可以單獨修改。

創建一個名為 MyApp 的可組合項,該組合項中包含問候語。

@Composable private fun MyApp() {   Surface(color = MaterialTheme.colors.background) {       Greeting("Android")   } }

這樣一來,由於現在可以重複使用 MyApp 可組合項,您就可以省去 onCreate 回調和預覽,從而避免重複編寫代碼。您的 MainActivity.kt 文件應如下所示:

import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.codelab.basicstep1.ui.theme.BasicsCodelabTheme ​ class MainActivity : ComponentActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContent {           BasicsCodelabTheme {               MyApp()           }       }   } } ​ @Composable private fun MyApp() {   Surface(color = MaterialTheme.colors.background) {       Greeting("Android")   } } ​ @Composable private fun Greeting(name: String) {   Surface(color = MaterialTheme.colors.primary) {       Text(text = "Hello $name!", modifier = Modifier.padding(24.dp))   } } ​ @Preview(showBackground = true) @Composable private fun DefaultPreview() {   BasicsCodelabTheme {       MyApp()   } }

創建列和行

Compose 中的三個基本標準佈局元素是 ColumnRowBox 可組合項。

518dbfad23ee1b05.png

它們是接受可組合內容的可組合函數,因此您可以在其中放置項目。例如,Column 中的每個子級都將垂直放置。

// Don't copy over Column {   Text("First row")   Text("Second row") }

現在嘗試更改 Greeting,使其顯示包含兩個文本元素的列,如以下示例中所示:

e42bf870995a84d2.png

請注意,您可能需要移動周圍的內邊距。

將您的結果與此解決方案進行比較:

import androidx.compose.foundation.layout.Column ... ​ @Composable private fun Greeting(name: String) {   Surface(color = MaterialTheme.colors.primary) {       Column(modifier = Modifier.padding(24.dp)) {           Text(text = "Hello,")           Text(text = name)       }   } }

Compose 和 Kotlin

可組合函數可以像 Kotlin 中的其他函數一樣使用。這會使界面構建變得非常有效,因為您可以添加語句來影響界面的顯示方式。

例如,您可以使用 for 循環向 Column 中添加元素:

@Composable fun MyApp(names: List<String> = listOf("World", "Compose")) {   Column {       for (name in names) {           Greeting(name = name)       }   } }

b6265492ef236d70.png

您尚未設置可組合項的尺寸,也未對可組合項的大小添加任何限制,因此每一行僅佔用可能的最小空間,預覽時的效果也是如此。讓我們更改預覽效果,以模擬小屏幕手機的常見寬度 320dp。按如下所示向 @Preview 註解添加 widthDp 參數:

@Preview(showBackground = true, widthDp = 320) @Composable fun DefaultPreview() {   BasicsCodelabTheme {       MyApp()   } }

8722ec524e694ba5.png

修飾符在 Compose 中使用得非常廣泛,現在我們來練習更高級的用法:嘗試使用 fillMaxWidthpadding 修飾符複製以下佈局。

fd7cb2daa600875.png

現在,將您的代碼與解決方案進行比較:

@Composable fun MyApp(names: List<String> = listOf("World", "Compose")) {   Column(modifier = Modifier.padding(vertical = 4.dp)) {       for (name in names) {           Greeting(name = name)       }   } } ​ @Composable private fun Greeting(name: String) {   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {           Text(text = "Hello, ")           Text(text = name)       }   } }@Composablefun MyApp(names: List<String> = listOf("World", "Compose")) {   Column(modifier = Modifier.padding(vertical = 4.dp)) {       for (name in names) {           Greeting(name = name)       }   }}@Composableprivate fun Greeting(name: String) {   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {           Text(text = "Hello, ")           Text(text = name)       }   }}

請注意:

  • 修飾符可以包含重載,因而具有相應的優勢,例如您可以指定不同的方式來創建內邊距。
  • 若要向一個元素添加多個修飾符,您只需要將它們鏈接起來即可。

有多種方式可以實現此結果,因此,如果您的代碼與此代碼段不同,並不表示您的代碼就是錯的。不過,為了繼續完成此 Codelab,仍請複製並粘貼此代碼。

添加按鈕

接下來,您將添加一個用於展開 Greeting 的可點擊元素,因此需要先添加對應的按鈕。您的目標是要創建以下佈局:

e74f07b36865a878.png

Button 是 Material 軟件包提供的一種可組合項,它採用可組合項作為最後一個參數。由於尾隨 lambda 可以移到括號之外,因此您可以向按鈕添加任何內容作為子級,例如 Text

// Don't copy yet Button(   onClick = { } // You'll learn about this callback later ) {   Text("Show less") }

注意:Compose 根據 Material Design 按鈕規範提供了不同類型的 ButtonButtonOutlinedButtonTextButton。在本示例中,您將使用包圍 Text 作為 Button 內容的 OutlinedButton

為了實現這一點,您需要學習如何在行尾放置可組合項。由於沒有 alignEnd 修飾符,因此您需要在開始時為該可組合項賦予一定的 weightweight 修飾符會讓元素填滿所有可用空間,使其“具有彈性”,也就是會推開其他沒有權重的元素(即“無彈性”元素)。該修飾符還會使 fillMaxWidth 修飾符變得多餘。

現在嘗試添加該按鈕,並按照上述圖片中所示放置該按鈕。

下面列出了對應的解決方案代碼:

import androidx.compose.material.Button import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember ... ​ @Composable private fun Greeting(name: String) { ​   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier.weight(1f)) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { /* TODO */ }           ) {               Text("Show more")           }       }   } }

Compose 中的狀態

在本部分中,您將向屏幕中添加一些互動。到目前為止,您已經創建了一些靜態佈局,現在您要讓它們響應用户更改,以達到下面的效果:

ae3c993d793aa843.gif

在開始瞭解如何使按鈕可點擊以及如何調整內容大小之前,您需要在某個位置存儲某個值,用於指示每項內容是否展開(即內容的狀態)。由於我們需要為每條問候語設定這兩個值之一,因此其邏輯位置位於 Greeting 可組合項中。我們來看看此 expanded 布爾值及其在代碼中的使用方式:

// Don't copy over @Composable private fun Greeting(name: String) {   var expanded = false // Don't do this! ​   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier.weight(1f)) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { expanded = !expanded }           ) {               Text(if (expanded) "Show less" else "Show more")           }       }   } }

請注意,我們還添加了 onClick 操作和動態按鈕文本。稍後會進一步介紹。

但是,此設置無法按預期發揮作用。為 expanded 變量設置不同的值不會使 Compose 將其檢測為狀態更改,因此不會產生任何效果。

Compose 應用通過調用可組合函數將數據轉換為界面。如果您的數據發生變化,Compose 會使用新數據重新執行這些函數,從而創建更新後的界面,此過程稱為重組。Compose 還會查看各個可組合項需要哪些數據,以便只需重組數據發生了變化的組件,而避免重組未受影響的組件。

正如 Compose 編程思想一文中所述:

可組合函數可以按任意順序頻繁執行,因此您不能以代碼的執行順序或該函數的重組次數為判斷依據。

更改此變量不會觸發重組的原因是 Compose 並未跟蹤此更改。此外,每次調用 Greeting 時,都會將該變量重置為 false。

如需向可組合項添加內部狀態,您可以使用 mutableStateOf 函數,該函數可讓 Compose 重組讀取該 State 的函數。

StateMutableState 是兩個接口,它們具有特定的值,每當該值發生變化時,它們就會觸發界面更新(重組)。

import androidx.compose.runtime.mutableStateOf ... ​ // Don't copy over @Composable fun Greeting() {   val expanded = mutableStateOf(false) // Don't do this! }

但是,不能只是mutableStateOf 分配給可組合項中的某個變量。如前所述,重組可能會隨時發生,這會再次調用可組合項,從而將狀態重置為值為 false 的新可變狀態。

如需在重組後保留狀態,請使用 remember 記住可變狀態。

import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember ... ​ @Composable fun Greeting() {   val expanded = remember { mutableStateOf(false) }   ... }

remember 可以起到保護作用,防止狀態在重組時被重置。

請注意,如果從屏幕的不同部分調用同一可組合項,則會創建不同的界面元素,且每個元素都會擁有自己的狀態版本。您可以將內部狀態視為類中的私有變量。

可組合函數會自動“訂閲”狀態。如果狀態發生變化,讀取這些字段的可組合項將會重組以顯示更新。

更改狀態和響應狀態更改

您可能已經注意到,為了更改狀態,Button 具有一個名為 onClick 的參數,但它不接受值,而接受函數

您可能不熟悉以這種方式使用的函數,這其實就是一種在 Compose 中廣泛使用的非常強大的 Kotlin 功能。函數是 Kotlin 中的首要元素,您可以將它們分配給某個變量,傳遞給其他函數,甚至可以從它們自身返回函數。您可以在此處瞭解 Compose 如何使用 Kotlin 功能

如需詳細瞭解如何定義和實例化函數,請參閲函數類型文檔

您可以通過為“onClick”指定 lambda 表達式,定義點擊時將執行的操作。例如,切換展開狀態的值,並根據該值顯示不同的文本。

OutlinedButton(               onClick = { expanded.value = !expanded.value },           ) {               Text(if (expanded.value) "Show less" else "Show more")           }

如果在模擬器中運行應用,您會看到點擊該按鈕時,expanded 會切換,從而觸發重組該按鈕內的文本。每個 Greeting 都具有自己的展開狀態,因為它們屬於不同的界面元素。

825dd6d6f98bff05.gif

到目前為止的代碼:

@Composable private fun Greeting(name: String) {   val expanded = remember { mutableStateOf(false) } ​   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier.weight(1f)) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { expanded.value = !expanded.value }           ) {               Text(if (expanded.value) "Show less" else "Show more")           }       }   } }

展開內容

現在,我們來根據請求實際展開內容。添加一個依賴於狀態的額外變量:

@Composable private fun Greeting(name: String) { ​   val expanded = remember { mutableStateOf(false) } ​   val extraPadding = if (expanded.value) 48.dp else 0.dp ...

您無需在重組後記住 extraPadding,因為它僅執行簡單的計算。

現在我們可以將新的內邊距修飾符應用於 Column:

@Composable private fun Greeting(name: String) { ​   val expanded = remember { mutableStateOf(false) } ​   val extraPadding = if (expanded.value) 48.dp else 0.dp ​   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier               .weight(1f)               .padding(bottom = extraPadding)           ) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { expanded.value = !expanded.value }           ) {               Text(if (expanded.value) "Show less" else "Show more")           }       }   } }

如果在模擬器上運行,您應該會看到每項內容均可單獨展開:

ae3c993d793aa843.gif

狀態提升

在可組合函數中,被多個函數讀取或修改的狀態應位於共同祖先實體中,此過程稱為狀態提升。“提升”是指“提高”或“升級”。

使狀態可提升,可以避免複製狀態和引入 bug,有助於重複使用可組合項,並大大降低可組合項的測試難度。相反,不需要由可組合項的父級控制的狀態則不應該被提升。可信來源屬於該狀態的創建者和控制者。

例如,讓我們來為應用創建一個初始配置屏幕。

8c0da5d9a631ba97.png

將以下代碼添加到 MainActivity.kt

import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.foundation.layout.Arrangement ​ ... ​ @Composable fun OnboardingScreen() {   // TODO: This state should be hoisted   var shouldShowOnboarding by remember { mutableStateOf(true) } ​   Surface {       Column(           modifier = Modifier.fillMaxSize(),           verticalArrangement = Arrangement.Center,           horizontalAlignment = Alignment.CenterHorizontally       ) {           Text("Welcome to the Basics Codelab!")           Button(               modifier = Modifier.padding(vertical = 24.dp),               onClick = { shouldShowOnboarding = false }           ) {               Text("Continue")           }       }   } } ​ @Preview(showBackground = true, widthDp = 320, heightDp = 320) @Composable fun OnboardingPreview() {   BasicsCodelabTheme {       OnboardingScreen()   } }

此代碼包含多個新功能:

  • 您已經添加了一個名為 OnboardingScreen 的新可組合項以及一個新的預覽。構建項目時,您會發現您可以同時擁有多個預覽。我們還添加了一個固定高度,以驗證內容是否正確對齊。
  • 可以配置 Column,使其在屏幕中心顯示其內容。
  • shouldShowOnboarding 使用的是 by 關鍵字,而不是 =。這是一個屬性委託,可讓您無需每次都輸入 .value
  • 點擊該按鈕時,會將 shouldShowOnboarding 設為 false,儘管您並未從任何位置讀取該狀態。

現在,我們即可將這個新的初始配置屏幕添加到應用。我們希望該屏幕在應用啟動時顯示,然後在用户按“繼續”時隱藏。

在 Compose 中,您不會隱藏界面元素,因為不會將它們添加到組合中,因此它們也不會添加到 Compose 生成的界面樹中。您只需要使用簡單的 Kotlin 條件邏輯就可以做到這一點。例如,如需顯示初始配置屏幕或問候語列表,您需要執行以下操作:

// Don't copy yet @Composable fun MyApp() {   if (shouldShowOnboarding) { // Where does this come from?       OnboardingScreen()   } else {       Greetings()   } }

但是,我們無法訪問 shouldShowOnboarding。很明顯,我們需要與 MyApp 可組合項共享在 OnboardingScreen 中創建的狀態。

我們不會以某種方式與狀態的父級共享狀態值,而是會提升該狀態,也就是將該狀態移到需要訪問它的共同祖先實體中。

首先,將 MyApp 的內容移到名為 Greetings 的新可組合項中:

@Composable fun MyApp() {     Greetings() } ​ @Composable private fun Greetings(names: List<String> = listOf("World", "Compose")) {   Column(modifier = Modifier.padding(vertical = 4.dp)) {       for (name in names) {           Greeting(name = name)       }   } }

現在,添加相應的邏輯來顯示 MyApp 中的不同屏幕,並提升狀態。

@Composable fun MyApp() { ​   var shouldShowOnboarding by remember { mutableStateOf(true) } ​   if (shouldShowOnboarding) {       OnboardingScreen(/* TODO */)   } else {       Greetings()   } }

我們還需要與初始配置屏幕共享 shouldShowOnboarding,但我們不會直接傳遞它。與其讓 OnboardingScreen 更改狀態,不如讓它在用户點擊“Continue”按鈕時通知我們。

如何向上傳遞事件?通過向下傳遞迴調來傳遞。回調是這樣一類函數,它們以參數的形式傳遞給其他函數,並在事件發生時執行。

嘗試向初始配置屏幕添加定義為 onContinueClicked: () -> Unit 的函數參數,以便您可以從 MyApp 更改狀態。

解決方案:

@Composable fun MyApp() { ​   var shouldShowOnboarding by remember { mutableStateOf(true) } ​   if (shouldShowOnboarding) {       OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })   } else {       Greetings()   } } ​ @Composable fun OnboardingScreen(onContinueClicked: () -> Unit) { ​   Surface {       Column(           modifier = Modifier.fillMaxSize(),           verticalArrangement = Arrangement.Center,           horizontalAlignment = Alignment.CenterHorizontally       ) {           Text("Welcome to the Basics Codelab!")           Button(               modifier = Modifier                   .padding(vertical = 24.dp),               onClick = onContinueClicked           ) {               Text("Continue")           }       }   } }

通過向 OnboardingScreen 傳遞函數而不是狀態,可以提高該可組合項的可重用性,並防止狀態被其他可組合項更改。一般而言,這可以讓事情變得簡單。一個很好的例子就是,現在需要如何修改初始配置屏幕預覽來調用 OnboardingScreen

@Preview(showBackground = true, widthDp = 320, heightDp = 320) @Composable fun OnboardingPreview() {   BasicsCodelabTheme {       OnboardingScreen(onContinueClicked = {}) // Do nothing on click.   } }

onContinueClicked 分配給空 lambda 表達式就等於“什麼也不做”,這非常適合於預覽。

看起來已經越來越像一個真正的應用了,非常棒!

c8c6c011ec37fe84.gif

到目前為止的完整代碼:

import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.codelab.basics.ui.BasicsCodelabTheme ​ class MainActivity : ComponentActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContent {           BasicsCodelabTheme {               MyApp()           }       }   } } ​ @Composable fun MyApp() { ​   var shouldShowOnboarding by remember { mutableStateOf(true) } ​   if (shouldShowOnboarding) {       OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })   } else {       Greetings()   } } ​ @Composable fun OnboardingScreen(onContinueClicked: () -> Unit) { ​   Surface {       Column(           modifier = Modifier.fillMaxSize(),           verticalArrangement = Arrangement.Center,           horizontalAlignment = Alignment.CenterHorizontally       ) {           Text("Welcome to the Basics Codelab!")           Button(               modifier = Modifier.padding(vertical = 24.dp),               onClick = onContinueClicked           ) {               Text("Continue")           }       }   } } ​ @Composable private fun Greetings(names: List<String> = listOf("World", "Compose")) {   Column(modifier = Modifier.padding(vertical = 4.dp)) {       for (name in names) {           Greeting(name = name)       }   } } ​ @Preview(showBackground = true, widthDp = 320, heightDp = 320) @Composable fun OnboardingPreview() {   BasicsCodelabTheme {       OnboardingScreen(onContinueClicked = {})   } } ​ @Composable private fun Greeting(name: String) { ​   val expanded = remember { mutableStateOf(false) } ​   val extraPadding = if (expanded.value) 48.dp else 0.dp ​   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier               .weight(1f)               .padding(bottom = extraPadding)           ) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { expanded.value = !expanded.value }           ) {               Text(if (expanded.value) "Show less" else "Show more")           }       }   } } ​ @Preview(showBackground = true, widthDp = 320) @Composable fun DefaultPreview() {   BasicsCodelabTheme {       Greetings()   } }

創建高效延遲列表

現在,我們來讓名稱列表更真實。到目前為止,您已經在 Column 中顯示了兩條問候語。但是,它可以處理成千上萬條問候語嗎?

Greetings 參數中的默認列表值更改為使用其他列表構造函數,這使您可以設置列表的大小並使用其 lambda 中包含的值來填充列表(這裏的 $it 代表列表索引):

names: List<String> = List(1000) { "$it" }

這會創建 1000 條問候語,即使屏幕上放不下這些問候語。顯然,這樣做效果並不好。您可以嘗試在模擬器上運行此代碼(警告:此代碼可能會使模擬器卡住)。

為顯示可滾動列,我們需要使用 LazyColumnLazyColumn 只會渲染屏幕上可見的內容,從而在渲染大型列表時提升效率。

注意:LazyColumnLazyRow 相當於 Android View 中的 RecyclerView

在其基本用法中,LazyColumn API 會在其作用域內提供一個 items 元素,並在該元素中編寫各項內容的渲染邏輯:

import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items ... ​ @Composable private fun Greetings(names: List<String> = List(1000) { "$it" } ) {   LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {       items(items = names) { name ->           Greeting(name = name)       }   } }

注意:請確保導入 androidx.compose.foundation.lazy.items,因為 Android Studio 默認會選擇另一個 items 函數。

注意LazyColumn 不會像 RecyclerView 一樣回收其子級。它會在您滾動它時發出新的可組合項,並保持高效運行,因為與實例化 Android Views 相比,發出可組合項的成本相對較低。

b9ffef51a5fbc8ca.gif

保留狀態

我們的應用存在一個問題:如果您在設備上運行該應用,點擊按鈕,然後旋轉屏幕,系統會再次顯示初始配置屏幕。remember 函數僅在可組合項包含在組合中時起作用。旋轉屏幕後,整個 activity 都會重啟,所有狀態都將丟失。當發生任何配置更改或者進程終止時,也會出現這種情況。

您可以使用 rememberSaveable,而不使用 remember。這會保存每個在配置更改(如旋轉)和進程終止後保留下來的狀態。

現在,將 shouldShowOnboarding 中的 remember 替換為 rememberSaveable

var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

運行應用,旋轉屏幕,更改為深色模式,或者終止進程。除非您之前退出了應用,否則系統不會顯示初始配置屏幕。

d7802a1acb90beba.gif

演示發生配置更改(切換到深色模式)後不會再次顯示初始配置屏幕。

到目前為止,您已經編寫了 120 行左右的代碼,您可以顯示一個包含大量內容項的高效滾動列表,並且每項內容都有自己的狀態。此外,如您所見,您不需要編寫額外的代碼就可以讓應用完美呈現深色模式。稍後您將學習主題設置。

為列表添加動畫效果

在 Compose 中,有多種方式可以為界面添加動畫效果:從用於添加簡單動畫的高階 API 到用於實現完全控制和複雜過渡的低階方法,不一而足。您可以在該文檔中瞭解相關信息。

在本部分中,您將使用一個低階 API,但不用擔心,它們也可以非常簡單。下面我們來為已經實現的尺寸變化添加動畫效果:

83bbc35a3bd4b1b2.gif

為此,您將使用 animateDpAsState 可組合項。該可組合項會返回一個 State 對象,該對象的 value 會被動畫持續更新,直到動畫播放完畢。該可組合項需要一個類型為 Dp 的“目標值”。

創建一個依賴於展開狀態的動畫 extraPadding。此外,我們還需要使用屬性委託(by 關鍵字):

@Composable private fun Greeting(name: String) { ​   var expanded by remember { mutableStateOf(false) } ​   val extraPadding by animateDpAsState(       if (expanded) 48.dp else 0.dp   )   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier               .weight(1f)               .padding(bottom = extraPadding)           ) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { expanded = !expanded }           ) {               Text(if (expanded) "Show less" else "Show more")           } ​       }   } }

運行應用並查看該動畫的效果。

注意:如果您展開第 1 項內容,然後滾動到第 20 項內容,再返回到第 1 項內容,您會發現第 1 項內容已恢復為原始尺寸。如果需要,您可以使用 rememberSaveable 保存此數據,但為了使示例保持簡單,我們不這樣做。

animateDpAsState 接受可選的 animationSpec 參數供您自定義動畫。讓我們來做一些更有趣的嘗試,比如添加基於彈簧的動畫:

@Composable private fun Greeting(name: String) { ​   var expanded by remember { mutableStateOf(false) } ​   val extraPadding by animateDpAsState(       if (expanded) 48.dp else 0.dp,       animationSpec = spring(           dampingRatio = Spring.DampingRatioMediumBouncy,           stiffness = Spring.StiffnessLow       )   ) ​   Surface(   ...           Column(modifier = Modifier               .weight(1f)               .padding(bottom = extraPadding.coerceAtLeast(0.dp)) ​   ... ​   ) }

請注意,我們還要確保內邊距不會為負數,否則可能會導致應用崩潰。這會引入一個細微的動畫 bug,我們稍後會在收尾部分進行修復。

spring 規範不接受任何與時間有關的參數。它僅依賴於物理屬性(阻尼和剛度),使動畫更自然。立即運行該應用,查看新動畫的效果:

c14f0b8f617d21eb.gif

使用 animate*AsState 創建的任何動畫都是可中斷的。這意味着,如果目標值在動畫播放過程中發生變化,animate*AsState 會重啟動畫並指向新值。中斷在基於彈簧的動畫中看起來尤其自然:

f72863865f685a62.gif

如果您想探索不同類型的動畫,請嘗試為 spring 提供不同的參數,嘗試使用不同的規範(tweenrepeatable)和不同的函數(animateColorAsState不同類型的動畫 API)。

此部分的完整代碼

import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Button import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.codelab.basics.ui.BasicsCodelabTheme ​ class MainActivity : ComponentActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContent {           BasicsCodelabTheme {               MyApp()           }       }   } } ​ @Composable fun MyApp() { ​   var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) } ​   if (shouldShowOnboarding) {       OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })   } else {       Greetings()   } } ​ @Composable fun OnboardingScreen(onContinueClicked: () -> Unit) { ​   Surface {       Column(           modifier = Modifier.fillMaxSize(),           verticalArrangement = Arrangement.Center,           horizontalAlignment = Alignment.CenterHorizontally       ) {           Text("Welcome to the Basics Codelab!")           Button(               modifier = Modifier.padding(vertical = 24.dp),               onClick = onContinueClicked           ) {               Text("Continue")           }       }   } } ​ @Composable private fun Greetings(names: List<String> = List(1000) { "$it" } ) {   LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {       items(items = names) { name ->           Greeting(name = name)       }   } } ​ @Preview(showBackground = true, widthDp = 320, heightDp = 320) @Composable fun OnboardingPreview() {   BasicsCodelabTheme {       OnboardingScreen(onContinueClicked = {})   } } ​ @Composable private fun Greeting(name: String) { ​   var expanded by remember { mutableStateOf(false) } ​   val extraPadding by animateDpAsState(       if (expanded) 48.dp else 0.dp,       animationSpec = spring(           dampingRatio = Spring.DampingRatioMediumBouncy,           stiffness = Spring.StiffnessLow       )   )   Surface(       color = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       Row(modifier = Modifier.padding(24.dp)) {           Column(modifier = Modifier               .weight(1f)               .padding(bottom = extraPadding.coerceAtLeast(0.dp))           ) {               Text(text = "Hello, ")               Text(text = name)           }           OutlinedButton(               onClick = { expanded = !expanded }           ) {               Text(if (expanded) "Show less" else "Show more")           }       }   } } ​ @Preview(showBackground = true, widthDp = 320) @Composable fun DefaultPreview() {   BasicsCodelabTheme {       Greetings()   } }

設置應用的樣式和主題

到目前為止,您還沒有為任何可組合項設置過樣式,但已經獲得了一個不錯的默認效果,包括支持深色模式!下面我們來了解一下 BasicsCodelabThemeMaterialTheme

如果您打開 ui/Theme.kt 文件,您會看到 BasicsCodelabTheme 在其實現中使用了 MaterialTheme

@Composable fun BasicsCodelabTheme(   darkTheme: Boolean = isSystemInDarkTheme(),   content: @Composable () -> Unit ) {   val colors = if (darkTheme) {       DarkColorPalette   } else {       LightColorPalette   } ​   MaterialTheme(       colors = colors,       typography = typography,       shapes = shapes,       content = content   ) }

MaterialTheme 是一個可組合函數,體現了 Material Design 規範中的樣式設置原則。樣式設置信息會逐級向下傳遞到位於其 content 內的組件,這些組件會讀取該信息來設置自身的樣式。您在界面中已經使用了 BasicsCodelabTheme,如下所示:

BasicsCodelabTheme {       MyApp()   }

由於 BasicsCodelabThemeMaterialTheme 包圍在其內部,因此 MyApp 會使用該主題中定義的屬性來設置樣式。從任何後代可組合項中都可以檢索 MaterialTheme 的三個屬性:colorstypographyshapes。使用它們設置其中一個 Text 的標題樣式:

Column(modifier = Modifier               .weight(1f)               .padding(bottom = extraPadding.coerceAtLeast(0.dp))           ) {               Text(text = "Hello, ")               Text(text = name, style = MaterialTheme.typography.h4)           }

上例中的 Text 可組合項會設置新的 TextStyle。您可以創建自己的 TextStyle,也可以使用 MaterialTheme.typography 檢索由主題定義的樣式(首選)。此結構支持您訪問由 Material 定義的文本樣式,例如 h1-h6body1,body2captionsubtitle1 等。在本例中,您將使用主題中定義的 h4 樣式。

下面我們構建應用來查看採用新樣式的文本:

471658bc17da5b67.png

通常來説,最好是將顏色、形狀和字體樣式放在 MaterialTheme 中。例如,如果對顏色進行硬編碼,將會很難實現深色模式,並且需要進行大量修正工作,而這很容易造成錯誤。

不過,有時除了選擇顏色和字體樣式,您還可以基於現有的顏色或樣式進行設置。

為此,您可以使用 copy 函數修改預定義的樣式。將數字加粗:

Text(                   text = name,                   style = MaterialTheme.typography.h4.copy(                       fontWeight = FontWeight.ExtraBold                   )               )

這樣一來,如果您需要更改 h4 的字體系列或其他任何屬性,就不必擔心出現細微偏差了。

現在,預覽窗口中的結果應如下所示:

3c9a6d5d0939c813.png

調整應用的主題

您可以在 ui 文件夾內的文件中找到與當前主題相關的所有內容。例如,我們到目前為止所使用的默認顏色均在 Color.kt 中定義。

首先,我們來定義新的顏色。將以下代碼添加到 Color.kt 中:

val Navy = Color(0xFF073042) val Blue = Color(0xFF4285F4) val LightBlue = Color(0xFFD7EFFE) val Chartreuse = Color(0xFFEFF7CF)

現在,將這些顏色分配給 Theme.kt 中的 MaterialTheme 的調色板:

private val LightColorPalette = lightColors(   surface = Blue,   onSurface = Color.White,   primary = LightBlue,   onPrimary = Navy )

返回 MainActivity.kt 並刷新預覽,就會看到這些新顏色:

358b92c429c4c579.png

但您還未修改深色。在進行這項操作之前,我們需要先設置相應的預覽。使用 UI_MODE_NIGHT_YESDefaultPreview 添加額外的 @Preview 註解:

@Preview(   showBackground = true,   widthDp = 320,   uiMode = UI_MODE_NIGHT_YES,   name = "DefaultPreviewDark" ) @Preview(showBackground = true, widthDp = 320) @Composable fun DefaultPreview() {   BasicsCodelabTheme {       Greetings()   } }

系統隨即會添加一個深色模式的預覽。

7cfcdecdeccaf627.png

Theme.kt 中,定義針對深色的調色板:

private val DarkColorPalette = darkColors(   surface = Blue,   onSurface = Navy,   primary = Navy,   onPrimary = Chartreuse )

現在,我們已經為應用設置了主題和樣式!

351d2a0ff94056d1.png

Theme.kt 的最終代碼

import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material.MaterialTheme import androidx.compose.material.darkColors import androidx.compose.material.lightColors import androidx.compose.runtime.Composable ​ private val DarkColorPalette = darkColors(   surface = Blue,   onSurface = Navy,   primary = Navy,   onPrimary = Chartreuse ) ​ private val LightColorPalette = lightColors(   surface = Blue,   onSurface = Color.White,   primary = LightBlue,   onPrimary = Navy ) ​ @Composable fun BasicsCodelabTheme(   darkTheme: Boolean = isSystemInDarkTheme(),   content: @Composable () -> Unit ) {   val colors = if (darkTheme) {       DarkColorPalette   } else {       LightColorPalette   } ​   MaterialTheme(       colors = colors,       typography = typography,       shapes = shapes,       content = content   ) }

結尾

在此步驟中,您將實際運用已掌握的知識,並通過幾條提示來學習幾個新的概念。您將創建以下內容:

5dcc23167391e246.gif

用圖標替換按鈕

  • IconButton 可組合項與子級 Icon 結合使用。
  • 使用 material-icons-extended 工件中提供的 Icons.Filled.ExpandLessIcons.Filled.ExpandMore。將以下代碼行添加到 app/build.gradle 文件中的依賴項中。

implementation "androidx.compose.material:material-icons-extended:$compose_version"

  • 修改內邊距以修正對齊問題。
  • 為無障礙功能添加內容説明(請參閲下面的“使用字符串資源”)。

使用字符串資源

應該為“Show more”和“show less”提供內容説明,您可以通過簡單的 if 語句進行添加:

contentDescription = if (expanded) "Show less" else "Show more"

不過,硬編碼字符串的方式並不可取,應該從 strings.xml 文件中獲取字符串。

您可以通過對每個字符串使用“Extract string resource”(在 Android Studio 中的“Context Actions”中提供)來自動執行此操作。

或者,打開 app/src/res/values/strings.xml 並添加以下資源:

<string name="show_less">Show less</string><string name="show_more">Show more</string>

展開

“Composem ipsum”文字會出現又消失,這會觸發每張卡片的大小變化。

  • 將新的 Text 添加到 Greeting 中當內容展開時顯示的 Column 中。
  • 移除 extraPadding 並改為將 animateContentSize 修飾符應用於 Row。這會自動執行創建動畫的過程,而手動執行該過程會很困難。此外,也不需要再使用 coerceAtLeast

添加高度和形狀

  • 您可以結合使用 shadow 修飾符和 clip 修飾符來實現卡片外觀。不過,有一種 Material 可組合項也可以做到這一點:Card

最終代碼

import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.Button import androidx.compose.material.Card import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface import androidx.compose.material.Text import androidx.compose.material.icons.Icons.Filled import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.codelab.basics.R import com.codelab.basics.ui.BasicsCodelabTheme ​ class MainActivity : ComponentActivity() {   override fun onCreate(savedInstanceState: Bundle?) {       super.onCreate(savedInstanceState)       setContent {           BasicsCodelabTheme {               MyApp()           }       }   } } ​ @Composable private fun MyApp() {   var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) } ​   if (shouldShowOnboarding) {       OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })   } else {       Greetings()   } } ​ @Composable private fun OnboardingScreen(onContinueClicked: () -> Unit) {   Surface {       Column(           modifier = Modifier.fillMaxSize(),           verticalArrangement = Arrangement.Center,           horizontalAlignment = Alignment.CenterHorizontally       ) {           Text("Welcome to the Basics Codelab!")           Button(               modifier = Modifier.padding(vertical = 24.dp),               onClick = onContinueClicked           ) {               Text("Continue")           }       }   } } ​ @Composable private fun Greetings(names: List<String> = List(1000) { "$it" } ) {   LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {       items(items = names) { name ->           Greeting(name = name)       }   } } ​ @Composable private fun Greeting(name: String) {   Card(       backgroundColor = MaterialTheme.colors.primary,       modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)   ) {       CardContent(name)   } } ​ @Composable private fun CardContent(name: String) {   var expanded by remember { mutableStateOf(false) } ​   Row(       modifier = Modifier           .padding(12.dp)           .animateContentSize(               animationSpec = spring(                   dampingRatio = Spring.DampingRatioMediumBouncy,                   stiffness = Spring.StiffnessLow               )           )   ) {       Column(           modifier = Modifier               .weight(1f)               .padding(12.dp)       ) {           Text(text = "Hello, ")           Text(               text = name,               style = MaterialTheme.typography.h4.copy(                   fontWeight = FontWeight.ExtraBold               )           )           if (expanded) {               Text(                   text = ("Composem ipsum color sit lazy, " +                       "padding theme elit, sed do bouncy. ").repeat(4),               )           }       }       IconButton(onClick = { expanded = !expanded }) {           Icon(               imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,               contentDescription = if (expanded) {                   stringResource(R.string.show_less)               } else {                   stringResource(R.string.show_more)               } ​           )       }   } } ​ @Preview(   showBackground = true,   widthDp = 320,   uiMode = UI_MODE_NIGHT_YES,   name = "DefaultPreviewDark" ) @Preview(showBackground = true, widthDp = 320) @Composable fun DefaultPreview() {   BasicsCodelabTheme {       Greetings()   } } ​ @Preview(showBackground = true, widthDp = 320, heightDp = 320) @Composable fun OnboardingPreview() {   BasicsCodelabTheme {       OnboardingScreen(onContinueClicked = {})   } }

以上就是 Compose 的全部基礎知識了,如果想要繼續進階 Android的Jetpack Compose 技術,或者想學習更多Android前沿技術可以私信《Android核心進階技術手冊》獲取進行進階學習。

文末

學到這裏,那麼恭喜你Compose 的全部基礎知識你已經全部掌握,如有許多沒學會的,可以收藏此篇文章,或者點擊上方學習筆記。可以方便重複學習。

啟動新的 Compose 項目

如需啟動新的 Compose 項目,請打開 Android Studio,然後選擇 Start a new Android Studio project,如下所示:

f5980dbff6f0fb7c.jpeg

如果系統未顯示上述界面,請依次進入 File > New > New Project。

創建新項目時,請從可用模板中選擇 Empty Compose Activity。

a67ba73a4f06b7ac.png

點擊 Next,然後照常配置項目,並將其命名為 Basics Codelab。請確保您選擇的 **minimumSdkVersion 至少為 API 級別 21,這是 Compose 支持的最低 API 級別。

注意:如需詳細瞭解如何使用空 activity 設置 Compose,或如何將其添加到現有項目,可以添加前面資料手冊瞭解詳情。

選擇 Empty Compose Activity 模板後,會在項目中為您生成以下代碼:

  • 該項目已配置為使用 Compose。
  • 已創建 AndroidManifest.xml 文件。
  • build.gradle 和 app/build.gradle 文件包含 Compose 所需的選項和依賴項。

同步項目後,請打開 MainActivity.kt 並查看代碼。

``` class MainActivity : ComponentActivity() {     override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContent {             BasicsCodelabTheme {                 // A surface container using the 'background' color from the theme                 Surface(                     modifier = Modifier.fillMaxSize(),                     color = MaterialTheme.colors.background                 ) {                     Greeting("Android")                 }             }         }     } }

@Composable fun Greeting(name: String) {     Text(text = "Hello $name!") }

@Preview(showBackground = true) @Composable fun DefaultPreview() {     BasicsCodelabTheme {         Greeting("Android")     } } ```

警告:setContent 中使用的應用主題取決於項目名稱。本 Codelab 假定項目名稱為 LayoutsCodelab。**如果您從 Codelab 中複製並粘貼代碼,請記得使用 ui/Theme.kt 文件中提供的主題名稱來更新 BasicsCodelabTheme。本 Codelab 後面會講到如何設置主題。