Hướng dẫn

Hướng dẫn về Jetpack Compose

Jetpack Compose là một bộ công cụ hiện đại giúp xây dựng giao diện người dùng gốc của Android. Jetpack Compose đơn giản hoá và đẩy nhanh quá trình phát triển giao diện người dùng trên Android nhờ mã ngắn hơn, các công cụ mạnh mẽ và API Kotlin trực quan.

Trong hướng dẫn này, bạn sẽ tạo một thành phần giao diện người dùng đơn giản có các hàm khai báo. Bạn sẽ không chỉnh sửa bất kỳ bố cục XML nào hoặc không sử dụng Layout Editor. Thay vào đó, bạn sẽ gọi các hàm có khả năng kết hợp để xác định phần tử mình muốn và trình biên dịch Compose sẽ làm phần việc còn lại.

Bản xem trước đầy đủ
Bản xem trước đầy đủ

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 thành phần văn bản

Để bắt đầu, hãy tải xuống 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 Dự án mới, sau đó trong danh mục Điện thoại và Máy tính bảng, chọn Empty Compose Activity (Hoạt động Compose 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.material.Text

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}
  
hiện nội dung xem trước
ẩn nội dung xem trước

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!")
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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")
}
  
hiện nội dung xem trước
ẩn nội dung xem trước

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
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Text

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}
  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
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!")
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
import androidx.compose.ui.tooling.preview.Preview

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

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}
  
hiện nội dung xem trước
ẩn nội dung xem trước
Xem trước hàm có khả năng kết hợp trong Android Studio

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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
    )
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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

Hàm Column cho phép bạn sắp xếp các phần tử theo chiều dọc. Thêm Column vào hàm MessageCard.
Bạn có thể sử dụng Row để sắp xếp các mục theo chiều ngang và Box để nhóm các phần tử.

// ...
import androidx.compose.foundation.layout.Column

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

hiện nội dung xem trước
ẩn nội dung xem trước

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)
        }
  
    }
  
}
  
hiện nội dung xem trước
ẩn nội dung xem trước

Đị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)
        }
    }
}
  
hiện nội dung xem trước
ẩn nội dung 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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
    )
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
Xem trước 2 thành phần kết hợp Text (Văn bản) trùng lặp
// ...
import androidx.compose.foundation.layout.Column

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

hiện nội dung xem trước
ẩn nội dung xem trước
// ...
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)
        }
  
    }
  
}
  
hiện nội dung xem trước
ẩn nội dung xem trướ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)
        }
    }
}
  
hiện nội dung xem trước
ẩn nội dung xem trước

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 của bạn 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 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 @PreviewsetContent. 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, TypographyShape. 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("Colleague", "Take a look at Jetpack Compose, it's great!")
            )
        }
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

Màu

Sử dụng MaterialTheme.colors để 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.

Định kiểu cho tiêu đề và thêm đường viền cho hình ảnh.

// ...
import androidx.compose.foundation.border
import androidx.compose.material.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
           )

           Spacer(modifier = Modifier.height(4.dp))
           Text(text = msg.body)
       }
   }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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.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
           )
       }
   }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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.material.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.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
               )
           }
       }
   }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}
  
hiện nội dung xem trước
ẩn nội dung xem trước

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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}
  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...

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("Colleague", "Take a look at Jetpack Compose, it's great!")
            )
        }
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
import androidx.compose.foundation.border
import androidx.compose.material.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
           )

           Spacer(modifier = Modifier.height(4.dp))
           Text(text = msg.body)
       }
   }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...

@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
           )
       }
   }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
import androidx.compose.material.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.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
               )
           }
       }
   }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
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("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}
  
hiện nội dung xem trước
ẩn nội dung xem trước
Hình 3. Bản xem trước hiển thị cả thành phần kết hợp theo giao diện sáng và giao diện tối.

Bài 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 trang thông tin tin nhắn

Cuộc trò chuyện chỉ có một tin nhắn cho cảm giác hơi cô đơn, vì vậy hãy thay đổi cuộc trò chuyện của bạ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 thông báo. Đối với trường hợp này, hãy sử dụng LazyColumn 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. Nhập tập dữ liệu mẫu này 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)
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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 remembermutableStateOf.

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ư remembermutableStateOf, 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 sẽ cần thêm các mục nhập sau để sử dụng đúng by. Alt+Enter hoặc Option+Enter sẽ thêm các mục nhập đó 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.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
                )
            }
        }
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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.colors.surface thành MaterialTheme.colors.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.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 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
                )
            }
        }
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
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)
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
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.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
                )
            }
        }
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước
// ...
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.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 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
                )
            }
        }
    }
}

  
hiện nội dung xem trước
ẩn nội dung xem trước

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.

Các bước tiếp theo

Thiết lập
Giờ thì bạn đã hoàn tất hướng dẫn về Compose và có thể bắt đầu tạo ứng dụng bằng Compose.
Lộ trình
Hãy khám phá lộ trình của chúng tôi gồm các lớp học lập trình và video giúp bạn tìm hiểu và nắm bắt thông tin về Jetpack Compose.