第 1 課:可組合函式
Jetpack Compose 是針對可組合函式所建構。這些函式可讓您以程式輔助的方式定義應用程式 UI,方法是說明其樣式及提供資料依附元件,而不是專注於 UI 的建構流程(初始化元素、將其附加至父項等)。如要建立可組合函式,只要在函式名稱中加入 @Composable
註解即可。
新增文字元素
請先下載最新版的 Android Studio,然後使用空白 Compose 活動範本建立應用程式。預設範本已包含一些 Compose 元素,不過我們還是進行逐步建立。
首先,系統會顯示「Hello World!」,新增文字元素至 onCreate
方法中。方法是定義內容區塊,然後呼叫 Text()
函式。setContent
區塊會定義活動的版面配置(我們稱為可組合函式)。可組合函式只能從其他可組合函式呼叫。
Jetpack Compose 使用 Kotlin 編譯器外掛程式,將這些可組合函式轉換為應用程式的 UI 元素。舉例來說,由 Compose UI 程式庫定義的 Text()
函式在螢幕上顯示文字標籤。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }

定義可組合函式
如要讓函式組成,請新增 @Composable
註解。如要試用這項功能,請定義傳送名稱的 MessageCard()
函式,使用此函式來設定文字元素。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }

在 Android Studio 中預覽函式
Android Studio 可讓您在 IDE 中預覽可組合函式,而不必將應用程式安裝至 Android 裝置或模擬器。可組合函式必須提供所有參數的預設值。因此,您無法直接預覽 MessageCard()
函式。我們來再使用名為 PreviewMessageCard()
的第二個函式,並使用適當的參數呼叫 MessageCard()
。在 @Composable
前加上 @Preview
註解。
@Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }

重建專案應用程式本身不會變更,因為新的 PreviewMessageCard()
函式不會在任何位置呼叫,但 Android Studio 會新增預覽視窗。這個視窗會顯示 UI 元素預覽,它由可組合函式建立,標記為 @Preview
註解。如要隨時更新預覽,請按一下預覽視窗頂端的重新整理按鈕。

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }

@Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }


新增多個文字
在此,我們建構了第一個可組合函式及預覽!為了探索更多 Jetpack Compose 功能,我們會建立簡單的訊息螢幕,列出一些動畫可展開的訊息清單。
首先,要顯示作者的姓名和訊息內容,讓訊息內容變得更豐富。們必須先變更可組合參數,以便接受 Message
物件(而非 String
),並另外在 MessageCard
可組合元素中加入另一個 Text
可組合元素。請務必一併更新預覽畫面:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard(Message("Android", "Jetpack Compose")) } } } data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Text(text = msg.author) Text(text = msg.body) } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) }

此程式碼會在內容檢視畫面中建立兩個文字元素。不過,由於我們並沒有提供任何排列方式的相關資訊,因此文字元素會彼此繪製,讓文字無法讀取。
使欄
Column
函式可讓您垂直排列元素。將 Column
新增至 MessageCard()
函式。
使用列可水平排列項目,方塊則可堆疊元素。
@Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }

新增圖片元素
新增寄件者的個人資料相片,讓訊息更加豐富。使用 Resource Manager 匯入相片庫的相片,或使用這張圖片。新增 Row
可組合元素以獲得結構良好的設計,其中必須含有 Image
可組合元素:
@Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }

設定版面配置
我們的訊息版面配置具備適當的結構,但其元素沒有足夠間距,且圖片太大!如要修飾或設定可組合元素,Compose 會使用輔助鍵。此功能可變更可組合元素的大小、版面配置、外觀或新增高階互動元素,例如將元素設為可點擊屬性。可以將這些元素鏈結以產生更為豐富的可組合元素。讓我們使用其中幾個來改善版面配置:
@Composable fun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.dp) // Clip image to be shaped as a circle .clip(CircleShape) ) // Add a horizontal space between the image and the column Spacer(modifier = Modifier.width(8.dp)) Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard(Message("Android", "Jetpack Compose")) } } } data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Text(text = msg.author) Text(text = msg.body) } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) }


@Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }

@Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }

@Composable fun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.dp) // Clip image to be shaped as a circle .clip(CircleShape) ) // Add a horizontal space between the image and the column Spacer(modifier = Modifier.width(8.dp)) Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }

使用質感設計
訊息設計現在支援版面配置,但使用體驗尚有待改進。
Jetpack Compose 提供質感設計及其 UI 元素,可立即使用。我們會運用質感設計樣式,改善 MessageCard 的可組合元素的外觀。
首先,請將 MessageCard
函式及在專案中建立的質感主題 ComposeTutorialTheme
進行包裝。請使用 @Preview
及 setContent
函式進行此操作。
質感設計是圍繞三大支柱建構而成:顏色、字型、形狀。讓我們逐一新增
注意:空白 Compose 活動會產生專案的預設主題,讓您自訂 MaterialTheme。如果您將專案命名為 ComposeTutorial 之外的名稱,則可在 ui.theme 套件中找到您的自訂主題。
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { MessageCard(Message("Android", "Jetpack Compose")) } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!") ) } }

顏色
使用封裝主題的顏色進行樣式設定很簡單,您可直接使用主題中的值進行顏色設定。
設定標題樣式並在圖片中加入框線:
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }

字體排版
MaterialTheme 提供質感字型圖形樣式,只要將樣式新增至文字可組合元素即可。
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.body2 ) } } }

形狀
可以使用形狀加入最終輕觸。我們也在訊息中加上邊框間距,讓版面配置更加完善。
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.body2 ) } } } }

啟用深色主題
你可以選擇啟用深色主題(或夜間模式),避免在夜間顯示明亮的螢幕,或是單純節省裝置電力。支援質感設計後,Jetpack Compose 預設可以處理深色主題。使用質感設計的顏色、文字和背景會自動適應深色背景。
您可以在檔案中建立多個預覽做為個別函式,或新增多個註解至相同函式。
以便新增預覽註解並啟用夜間模式。
@Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) } }

IDE 產生的 Theme.kt
檔案會定義淺色和深色主題的顏色選項。
目前為止,我們建立了一個訊息 UI 元素,可以顯示一張圖片和兩則不同樣式的文字,在淺色和深色主題中都沒有問題!
@Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) } }

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { MessageCard(Message("Android", "Jetpack Compose")) } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!") ) } }

@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }

@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.body2 ) } } }

@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.body2 ) } } } }

@Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) } }


建立訊息清單
只傳送一則訊息會感覺有點孤單,因此我們可以變更對話內容以便設定多則訊息。我們必須建立顯示多則訊息的 Conversation
函式。以這個用途來說,我們可以使用 Compose 的 LazyColumn
和 LazyRow.
這些可組合元素只會呈現在螢幕上顯示的元素,因此非常適合用於長清單。與此同時,也能避免使用 XML 版面配置的 RecyclerView
時的複雜度。
在這個程式碼片段中,您可以看到 LazyColumn
含有一個子項目。它使用 List
做為參數,而其 lambda 會接收名為 message
的參數(我們可以視需要將它命名)也就是 Message
的執行個體。簡單來說,針對所提供的 List
中的每個項目都會呼叫此 lambda。將此範例資料集匯入專案,即可快速啟動對話。
import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } }

展開訊息時以動畫形式呈現訊息
我們的對話越來越有趣。一起來試試動畫吧!我們新增了展開訊息功能,以便顯示較長的訊息,並以內容大小和背景顏色呈現動畫效果。如要儲存本機 UI 的狀態,我們必須時刻關注訊息是否已展開。我們會使用 remember
及 mutableStateOf
函式來追蹤狀態變更。
可組合函式可以使用 remember
將本機狀態儲存在記憶體中,並將值的變更傳送至 mutableStateOf
。使用這個狀態的可組合元素(及其子項)會在值更新時自動重新繪製。這就是所謂的重新撰寫。
使用 Compose 的狀態 API(例如 remember
和 mutableStateOf
),任何狀態變更都會自動更新 UI。
注意:必須新增下列匯入項目,才能正確使用 by
。使用 Alt + Enter 或 Option + Enter 鍵即可新增項目。
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } } } } @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.dp, ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.body2 ) } } } }
現在起,當按一下訊息時,即可以根據 isExpanded
變更訊息內容的背景。我們會使用 clickable
輔助鍵來處理可組合元素的點擊事件。如今不需要再切換 Surface
的背景顏色,而是會逐步將背景的值從 MaterialTheme.colors.surface
變更為 MaterialTheme.colors.primary
(反之亦然),以動畫方式呈現背景顏色。為此,我們會使用 animateColorAsState
函式。最後,我們將使用 animateContentSize
輔助鍵,為訊息容器大小建立流暢的動畫:
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // surfaceColor will be updated gradually from one color to the other val surfaceColor: Color by animateColorAsState( if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.dp, // surfaceColor color will be changing gradually from primary to surface color = surfaceColor, // animateContentSize will change the Surface size gradually modifier = Modifier.animateContentSize().padding(1.dp) ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.body2 ) } } } }
import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } }

class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } } } } @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.dp, ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.body2 ) } } } }
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) // We keep track if the message is expanded or not in this // variable var isExpanded by remember { mutableStateOf(false) } // surfaceColor will be updated gradually from one color to the other val surfaceColor: Color by animateColorAsState( if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.dp, // surfaceColor color will be changing gradually from primary to surface color = surfaceColor, // animateContentSize will change the Surface size gradually modifier = Modifier.animateContentSize().padding(1.dp) ) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), // If the message is expanded, we display all its content // otherwise we only display the first line maxLines = if (isExpanded) Int.MAX_VALUE else 1, style = MaterialTheme.typography.body2 ) } } } }
後續步驟
恭喜,您已完成 Compose 教學課程!你建立了簡潔的即時通訊螢幕,並在其中提供包含圖片和文字的可展開及動畫訊息清單,採用質感設計原則及深色主題,可供預覽,一切只需要不到 100 行程式碼!
你目前已瞭解的內容如下:
- 定義可組合函式
- 在組合中新增不同元素
- 使用版面配置元件建構 UI 元件
- 使用輔助鍵擴充可組合元件
- 建立高效率清單
- 持續追蹤並修改狀態
- 在可組合元件上新增使用者互動
- 在展開時為訊息加入動畫效果
如要深入瞭解這些步驟,請參閱下列資源。