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 後面會講到如何設定主題。