Bài học 1: Hàm có khả năng kết hợp
Jetpack Compose được xây dựng xung quanh các hàm có khả năng kết hợp. Các hàm này cho phép bạn xác định giao diện người dùng của ứng dụng theo cách lập trình bằng việc mô tả giao diện đó và cung cấp các phần phụ thuộc dữ liệu thay vì tập trung vào quy trình xây dựng giao diện người dùng (khởi chạy một phần tử, đính kèm phần tử đó vào phần tử mẹ, v.v.). Để tạo một hàm có khả năng kết hợp, chỉ cần thêm chú thích @Composable
vào tên hàm.
Thêm một phần tử văn bản
Để bắt đầu, hãy tải phiên bản mới nhất của Android Studio và tạo một ứng dụng bằng cách chọn New Project (Dự án mới), sau đó trong danh mục Phone and Tablet (Điện thoại và máy tính bảng), hãy chọn Empty Activity (Hoạt động trống). Đặt tên cho ứng dụng là ComposeTutorial rồi nhấp vào Finish (Hoàn tất). Mẫu mặc định đã chứa một số phần tử Compose, nhưng trong hướng dẫn này, bạn sẽ từng bước tạo các phần tử đó.
Đầu tiên, hãy hiển thị dòng văn bản "Xin chào thế giới!" bằng cách thêm một phần tử văn bản vào phương thức onCreate
. Bạn thực hiện việc này bằng cách xác định một khối nội dung và gọi hàm có khả năng kết hợp Text
Khối setContent
xác định bố cục của hoạt động mà trong đó, những hàm có khả năng kết hợp sẽ được gọi. Bạn chỉ gọi được hàm có khả năng kết hợp từ các hàm khác có khả năng kết hợp.
Jetpack Compose dùng một trình bổ trợ biên dịch Kotlin để biến những hàm có khả năng kết hợp này thành các thành phần trên giao diện người dùng của ứng dụng. Ví dụ: hàm có khả năng kết hợp Text
do thư viện Giao diện người dùng trong Compose xác định sẽ hiển thị một nhãn văn bản trên màn hình.
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-02.png?authuser=5&hl=vi)
Xác định hàm có khả năng kết hợp
Để một hàm có thể kết hợp, hãy thêm chú thích @Composable
.
Để thử làm vậy, hãy xác định một hàm MessageCard
được truyền cho một tên và sử dụng hàm đó để định cấu hình thành phần văn bản.
// ... import androidx.compose.runtime.Composable class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=5&hl=vi)
Xem trước hàm trong Android Studio
Chú thích @Preview
cho phép bạn xem trước các hàm có khả năng kết hợp trong Android Studio mà không cần phải tạo và cài đặt ứng dụng trên thiết bị Android hoặc trình mô phỏng. Bạn phải dùng chú thích cho một hàm có khả năng kết hợp không nhận các tham số. Vì lý do này, bạn không thể trực tiếp xem trước hàm MessageCard
. Thay vào đó, hãy tạo một hàm thứ hai có tên là PreviewMessageCard
. Hàm này sẽ gọi MessageCard
bằng một tham số thích hợp. Hãy thêm chú thích @Preview
trước @Composable
.
// ... import androidx.compose.ui.tooling.preview.Preview @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=5&hl=vi)
Xây dựng lại dự án. Bản thân ứng dụng không thay đổi vì chức năng PreviewMessageCard
mới không được gọi ở bất cứ đâu. Tuy nhiên, Android Studio sẽ thêm một cửa sổ xem trước để bạn có thể mở rộng bằng cách nhấp vào khung hiển thị (thiết kế/mã) phân tách. Cửa sổ này hiển thị bản xem trước các thành phần trên giao diện người dùng do hàm có khả năng kết hợp được đánh dấu bằng chú thích @Preview
tạo. Để cập nhật bản xem trước bất kỳ lúc nào, hãy nhấp vào nút làm mới ở đầu cửa sổ xem trước.
![Xem trước hàm có khả năng kết hợp trong Android Studio](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-04.png?authuser=5&hl=vi)
import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.material3.Text class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-02.png?authuser=5&hl=vi)
// ... import androidx.compose.runtime.Composable class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=5&hl=vi)
// ... import androidx.compose.ui.tooling.preview.Preview @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-03.png?authuser=5&hl=vi)
![Xem trước hàm có khả năng kết hợp trong Android Studio](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson1-04.png?authuser=5&hl=vi)
Bài 2: Bố cục
Các thành phần trên giao diện người dùng được phân cấp, thành phần này được chứa trong thành phần khác. Trong Compose, bạn xây dựng một hệ phân cấp giao diện người dùng bằng cách gọi hàm có khả năng kết hợp qua các hàm khác có khả năng kết hợp.
Thêm nhiều văn bản
Đến đây, bạn đã tạo được hàm có khả năng kết hợp và bản xem trước đầu tiên! Để khám phá thêm các tính năng khác của Jetpack Compose, bạn sẽ xây dựng một màn hình đơn giản nhằm tạo thông báo, trong đó chứa danh sách những thông báo có thể mở rộng với một số ảnh động.
Hãy bắt đầu bằng việc khiến các thành phần kết hợp thông báo trở nên giàu thông tin hơn bằng cách hiển thị tên tác giả và nội dung thông báo. Trước tiên, bạn cần thay đổi tham số thành phần kết hợp để chấp nhận đối tượng Message
thay vì một String
, sau đó thêm thành phần kết hợp Text
vào bên trong thành phần kết hợp MessageCard
. Đừng quên cập nhật cả bản xem trước.
// ... 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("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-02.png?authuser=5&hl=vi)
Mã này tạo hai phần tử văn bản bên trong chế độ xem nội dung. Tuy nhiên, vì bạn chưa cung cấp thông tin nào về cách sắp xếp nên các thành phần văn bản sẽ được sắp xếp chồng lên nhau, khiến văn bản không đọc được.
Sử dụng Cột
// ... import androidx.compose.foundation.layout.Column @Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-04.png?authuser=5&hl=vi)
Thêm một phần tử hình ảnh
Để thẻ thông báo của bạn giàu thông tin hơn, hãy thêm ảnh hồ sơ của người gửi. Sử dụng Trình quản lý tài nguyên để nhập hình ảnh có trong thư viện ảnh của bạn hoặc dùng mã này. Thêm thành phần kết hợp Row
để có thiết kế cấu trúc hợp lý và thành phần kết hợp Image
bên trong nó.
// ... import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row import androidx.compose.ui.res.painterResource @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) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-05.png?authuser=5&hl=vi)
Định cấu hình bố cục
Bố cục của thông báo có cấu trúc phù hợp nhưng các phần tử trong đó không được giãn cách hợp lý và hình ảnh quá lớn! Để trang trí hoặc định cấu hình một thành phần kết hợp, Compose sử dụng đối tượng sửa đổi. Những đối tượng này cho phép bạn thay đổi kích thước, bố cục, giao diện của thành phần kết hợp hoặc thêm các lượt tương tác cấp cao, chẳng hạn như tạo một thành phần có thể nhấp. Bạn có thể kết nối chúng thành chuỗi để tạo thành phần kết hợp giàu thông tin hơn. Bạn sẽ dùng một số mã để cải thiện bố cục.
// ... import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @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) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-06.png?authuser=5&hl=vi)
// ... 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("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-02.png?authuser=5&hl=vi)
![Xem trước 2 thành phần kết hợp Text (Văn bản) trùng lặp](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-03.png?authuser=5&hl=vi)
// ... import androidx.compose.foundation.layout.Column @Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-04.png?authuser=5&hl=vi)
// ... import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Row import androidx.compose.ui.res.painterResource @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) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-05.png?authuser=5&hl=vi)
// ... import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @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) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson2-06.png?authuser=5&hl=vi)
Bài học 3: Thiết kế Material Design
Compose được xây dựng để hỗ trợ các nguyên tắc của Material Design. Nhiều thành phần trên giao diện người dùng trong Compose triển khai Material Design ngay từ đầu. Trong bài học này, bạn sẽ định kiểu cho ứng dụng bằng các tiện ích Material Design.
Sử dụng Material Design
Thiết kế thông báo hiện đã có bố cục nhưng trông vẫn chưa ổn lắm.
Jetpack Compose triển khai thiết kế Material Design 3 và các thành phần trên giao diện người dùng ngay từ đầu. Bạn sẽ cải thiện giao diện của thành phần kết hợp MessageCard
bằng cách sử dụng thiết kế Material Design.
Để bắt đầu, hãy gói hàm MessageCard
bằng giao diện Material đã tạo trong dự án ComposeTutorialTheme
của bạn cũng như Surface
.
Thực hiện việc này trong cả hàm @Preview
và setContent
. Thao tác này cho phép thành phần kết hợp kế thừa các kiểu đã được xác định trong giao diện của ứng dụng, nhờ đó đảm bảo tính nhất quán trên ứng dụng.
Material Design được xây dựng dựa trên 3 trụ cột: Color
, Typography
và Shape
.
Bạn sẽ thêm từng cái một.
Lưu ý: Mẫu Hoạt động Compose trống sẽ tạo một giao diện mặc định cho dự án để bạn có thể tuỳ chỉnh MaterialTheme
.
Nếu đặt tên cho dự án khác với tên trong ComposeTutorial, thì bạn có thể tìm thấy giao diện tuỳ chỉnh của mình ở tệp Theme.kt
trong gói con ui.theme
.
// ... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Surface(modifier = Modifier.fillMaxSize()) { MessageCard(Message("Android", "Jetpack Compose")) } } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-02.png?authuser=5&hl=vi)
Màu
Sử dụng MaterialTheme.colorScheme
để tạo kiểu với các màu trong giao diện được gói. Bạn có thể sử dụng các giá trị này trong giao diện ở bất kỳ nơi nào cần tô màu. Ví dụ này sử dụng màu động cho giao diện (được xác định theo lựa chọn ưu tiên đối với thiết bị).
Bạn có thể thiết lập dynamicColor
thành false
trong tệp MaterialTheme.kt
để thay đổi điều này.
Định kiểu cho tiêu đề và thêm đường viền cho hình ảnh.
// ... import androidx.compose.foundation.border import androidx.compose.material3.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.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-03.png?authuser=5&hl=vi)
Kiểu chữ
Các kiểu chữ Material có sẵn trong MaterialTheme
, bạn chỉ cần thêm chúng vào các thành phần kết hợp Text
.
// ... @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.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.bodyMedium ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-04.png?authuser=5&hl=vi)
Hình dạng
Với Shape
, bạn có thể thêm những chi tiết hoàn thiện. Trước tiên, hãy gói nội dung thông báo bằng văn bản quanh thành phần kết hợp Surface
. Theo đó, bạn có thể tuỳ chỉnh hình dạng và độ cao của nội dung thông báo. Khoảng đệm cũng được thêm vào thông báo để có bố cục tốt hơn.
// ... import androidx.compose.material3.Surface @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.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.bodyMedium ) } } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-05.png?authuser=5&hl=vi)
Bật giao diện tối
Bạn có thể bật giao diện tối (hay chế độ ban đêm) để tránh hiển thị màn hình sáng (nhất là vào ban đêm) hoặc đơn giản là để tiết kiệm pin của thiết bị. Nhờ sự hỗ trợ từ Material Design, Jetpack Compose có thể xử lý giao diện tối theo mặc định. Việc sử dụng màu, văn bản và nền của Material Design sẽ tự động thích ứng với nền tối.
Bạn có thể tạo nhiều bản xem trước trong tệp dưới dạng những hàm riêng biệt hoặc thêm nhiều chú thích vào cùng một hàm.
Thêm chú thích xem trước mới và bật chế độ ban đêm.
// ... import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-06.png?authuser=5&hl=vi)
Các lựa chọn màu cho giao diện sáng và tối được xác định trong tệp Theme.kt
do IDE tạo.
Đến đây, bạn đã tạo thành phần trên giao diện người dùng cho thông báo hiển thị một hình ảnh và 2 văn bản với các kiểu khác nhau. Mọi thứ trông thật tuyệt trong cả giao diện sáng lẫn tối!
// ... import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-06.png?authuser=5&hl=vi)
// ... class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Surface(modifier = Modifier.fillMaxSize()) { MessageCard(Message("Android", "Jetpack Compose")) } } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-02.png?authuser=5&hl=vi)
// ... import androidx.compose.foundation.border import androidx.compose.material3.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.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-03.png?authuser=5&hl=vi)
// ... @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.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.bodyMedium ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-04.png?authuser=5&hl=vi)
// ... import androidx.compose.material3.Surface @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.colorScheme.primary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.bodyMedium ) } } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-05.png?authuser=5&hl=vi)
// ... import android.content.res.Configuration @Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { Surface { MessageCard( msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!") ) } } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-06.png?authuser=5&hl=vi)
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson3-07.png?authuser=5&hl=vi)
Bài học 4: Trang thông tin và ảnh động
Danh sách và ảnh động xuất hiện ở mọi nơi trong ứng dụng. Trong bài học này, bạn sẽ tìm hiểu cách ứng dụng Compose giúp dễ dàng tạo danh sách và thêm ảnh động thú vị.
Tạo danh sách tin nhắn
Cuộc trò chuyện chỉ có một tin nhắn tạo cảm giác hơi cô đơn, vì vậy chúng ta sẽ thay đổi cuộc trò chuyện để có nhiều tin nhắn hơn. Bạn cần tạo một hàm Conversation
để hiển thị nhiều tin nhắn. Đối với trường hợp này, hãy sử dụng LazyColumn
và LazyRow
của Compose. Những thành phần kết hợp này chỉ hiển thị các phần tử hiện trên màn hình nên được thiết kế để hoạt động hiệu quả với trang thông tin dài.
Trong đoạn mã này, bạn có thể thấy LazyColumn
có một mục con items
. Cần phải có List
làm tham số và hàm lambda của tham số này sẽ nhận được một tham số mà chúng ta đã đặt tên là message
(chúng ta có quyền tuỳ ý đặt tên cho tham số này), một bản sao của Message
.
Nói ngắn gọn, hàm lambda này được gọi cho mỗi mục của List
được cung cấp. Sao chép tập dữ liệu mẫu vào dự án để giúp nhanh chóng điều chỉnh cuộc trò chuyện.
// ... import androidx.compose.foundation.lazy.LazyColumn 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) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson4-02.png?authuser=5&hl=vi)
Tạo ảnh động cho thông báo trong khi mở rộng
Cuộc trò chuyện ngày càng thú vị hơn. Đã đến lúc phát ảnh động! Bạn sẽ bổ sung khả năng mở rộng thông báo để hiển thị một thông báo dài hơn, giúp hoạt hoá cả kích thước nội dung lẫn màu nền. Để lưu trữ trạng thái giao diện người dùng cục bộ này, bạn cần theo dõi xem thông báo có được mở rộng hay không. Để theo dõi sự thay đổi về trạng thái đó, bạn cần sử dụng các hàm remember
và mutableStateOf
.
Các hàm có khả năng kết hợp có thể lưu trữ trạng thái cục bộ trong bộ nhớ bằng remember
và theo dõi các thay đổi đối với giá trị được truyền đến mutableStateOf
. Thành phần kết hợp (và các yếu tố có thể kết hợp con) sử dụng trạng thái này sẽ được tự động vẽ lại khi giá trị đó cập nhật. Hành động này được gọi là tái kết hợp.
Khi bạn dùng các API trạng thái của Compose như remember
và mutableStateOf
, mọi thay đổi về trạng thái sẽ tự động cập nhật giao diện người dùng.
Lưu ý: Bạn cần thêm các lệnh nhập sau đây để sử dụng đúng cú pháp thuộc tính được uỷ quyền của Kotlin (từ khoá by
). Alt+Enter hoặc Option+Enter sẽ thêm các mục đó cho bạn.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
// ... import androidx.compose.foundation.clickable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.colorScheme.primary, 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.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 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.bodyMedium ) } } } }
Giờ đây, bạn có thể thay đổi nền của nội dung thông báo dựa trên isExpanded
khi nhấp vào một thông báo. Bạn sẽ sử dụng đối tượng sửa đổi clickable
để xử lý các sự kiện nhấp chuột trên thành phần kết hợp. Thay vì chỉ chuyển đổi màu nền của Surface
, bạn sẽ tạo ảnh động cho màu nền bằng cách sửa đổi dần giá trị của nền từ MaterialTheme.colorScheme.surface
thành MaterialTheme.colorScheme.primary
và ngược lại. Để thực hiện việc này, bạn cần sử dụng hàm animateColorAsState
. Cuối cùng, bạn sẽ dùng đối tượng sửa đổi animateContentSize
để tạo ảnh động một cách trơn tru cho kích thước vùng chứa thông báo:
// ... import androidx.compose.animation.animateColorAsState import androidx.compose.animation.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.colorScheme.secondary, 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 by animateColorAsState( if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 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.bodyMedium ) } } } }
// ... import androidx.compose.foundation.lazy.LazyColumn 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) } }
![](https://developer.android.com/static/develop/ui/compose/images/compose-tutorial/lesson4-02.png?authuser=5&hl=vi)
// ... import androidx.compose.foundation.clickable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember 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.colorScheme.primary, 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.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 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.bodyMedium ) } } } }
// ... import androidx.compose.animation.animateColorAsState import androidx.compose.animation.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.colorScheme.secondary, 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 by animateColorAsState( if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colorScheme.secondary, style = MaterialTheme.typography.titleSmall ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, shadowElevation = 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.bodyMedium ) } } } }
Các bước tiếp theo
Xin chúc mừng, bạn đã hoàn tất hướng dẫn Compose! Bạn đã xây dựng một màn hình trò chuyện đơn giản, hiển thị hiệu quả một danh sách thông báo có thể mở rộng và dưới dạng ảnh động chứa hình ảnh cũng như văn bản, được thiết kế theo nguyên tắc của Material Design với giao diện tối và bản xem trước – tất cả đều dựa trên chưa đến 100 dòng mã!
Sau đây là những điều bạn đã tìm hiểu cho đến thời điểm này:
- Xác định các hàm có khả năng kết hợp
- Thêm các thành phần trong nội dung kết hợp
- Định cấu trúc thành phần giao diện người dùng bằng cách sử dụng kết hợp bố cục
- Mở rộng nội dung kết hợp bằng cách sử dụng công cụ sửa đổi
- Tạo danh sách hiệu quả
- Theo dõi trạng thái và sửa đổi trạng thái
- Thêm lượt tương tác của người dùng trên một nội dung kết hợp
- Tạo ảnh động cho tin nhắn trong khi mở rộng
Nếu bạn muốn tìm hiểu sâu hơn về một số bước này, hãy khám phá các tài nguyên dưới đây.