Kiến thức cơ bản về Jetpack Compose

1. Trước khi bắt đầu

Jetpack Compose là một bộ công cụ hiện đại được thiết kế để đơn giản hoá quá trình phát triển giao diện người dùng. Công cụ này kết hợp một mô hình lập trình phản ứng ngắn gọn và dễ sử dụng với ngôn ngữ lập trình Kotlin. Công cụ này cũng mang tính khai báo đầy đủ, nghĩa là bạn mô tả giao diện người dùng bằng cách gọi loạt hàm chuyển đổi dữ liệu thành một hệ phân cấp giao diện người dùng. Khi dữ liệu cơ bản thay đổi, khung sẽ tự động thực thi lại các hàm này và cập nhật hệ phân cấp giao diện người dùng cho bạn.

Ứng dụng Compose bao gồm các hàm có khả năng kết hợp – là các hàm thông thường được đánh dấu bằng @Composable, chúng có thể gọi các hàm có khả năng kết hợp khác. Hàm là tất cả những gì bạn cần để tạo thành phần giao diện người dùng mới. Chú thích này sẽ hướng dẫn Compose thêm các tính năng hỗ trợ đặc biệt vào hàm để cập nhật và duy trì giao diện người dùng theo thời gian. Compose cho phép bạn xây dựng cấu trúc mã thành các phần nhỏ. Hàm có khả năng kết hợp thường được gọi là "thành phần kết hợp".

Bằng việc tạo thành phần kết hợp (composable) nhỏ sử dụng lại được, bạn có thể dễ dàng xây dựng thư viện phần tử giao diện người dùng có trong ứng dụng. Mỗi phần này chịu trách nhiệm cho một phần của màn hình, ngoài ra còn có thể được chỉnh sửa độc lập.

Để được hỗ trợ thêm khi tham gia lớp học lập trình này, hãy xem nội dung tập lập trình dưới đây:

Lưu ý: Các bước tập lập trình dưới đây sử dụng Material 2, còn lớp học lập trình này được cập nhật để sử dụng Material 3. Xin lưu ý rằng một số bước có thể khác nhau.

Điều kiện tiên quyết

  • Kinh nghiệm về cú pháp Kotlin, bao gồm cả hàm lambda.

Bạn sẽ thực hiện

Trong lớp học lập trình này, bạn sẽ tìm hiểu:

  • Compose là gì
  • Cách xây dựng giao diện người dùng bằng Compose
  • Cách quản lý trạng thái trong các hàm có khả năng kết hợp
  • Cách tạo danh sách hiệu suất
  • Cách thêm ảnh động
  • Cách tạo kiểu và giao diện cho ứng dụng

Bạn sẽ tạo một ứng dụng có màn hình giới thiệu cùng với danh sách các mục động mở rộng:

8d24a786bfe1a8f2.gif

Bạn cần có

2. Bắt đầu với một dự án Compose mới

Để bắt đầu một dự án Compose mới, hãy mở Android Studio.

Nếu bạn đang ở cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), vui lòng nhấp vào nút Start a new Android Studio (Bắt đầu dự án Android Studio mới). Nếu bạn đã mở một dự án Android Studio, hãy chọn File > New > New Project (Tệp > Mới > Dự án mới) trong thanh trình đơn.

Đối với dự án mới, hãy chọn Empty Activity (Hoạt động trống) trong số các mẫu có sẵn.

d12472c6323de500.png

Nhấp vào Next (Tiếp theo) rồi định cấu hình dự án như bình thường, gọi dự án là "Basics Codelab" (Lớp học lập trình cơ bản). Hãy nhớ chọn minimumSdkVersion tối thiểu là API cấp 21, đây là API tối thiểu mà Compose hỗ trợ.

Khi chọn mẫu Empty Activity (Hoạt động trống), đoạn mã sau đây sẽ được tạo cho bạn trong dự án:

  • Dự án đã được định cấu hình để sử dụng Compose.
  • Tệp AndroidManifest.xml đã được tạo.
  • Các tệp build.gradle.ktsapp/build.gradle.kts chứa các lựa chọn và phần phụ thuộc cần thiết cho Compose.

Sau khi đồng bộ hoá dự án, hãy mở MainActivity.kt và xem mã.

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.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

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

Trong phần tiếp theo, bạn sẽ thấy chức năng của mỗi phương thức cũng như cách cải thiện các phương thức đó để tạo bố cục linh hoạt và có thể sử dụng lại.

Giải pháp cho lớp học lập trình

Bạn có thể lấy đoạn mã cho giải pháp của lớp học lập trình này trên kho lưu trữ GitHub:

$ git clone https://github.com/android/codelab-android-compose

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp Zip:

Bạn sẽ tìm thấy mã giải pháp trong dự án BasicsCodelab. Bạn nên làm theo hướng dẫn từng bước trong lớp học lập trình theo tốc độ riêng và xem giải pháp nếu cần thiết. Xuyên suốt lớp học lập trình, bạn sẽ thấy các đoạn mã cần thêm vào dự án.

3. Bắt đầu với Compose

Xem qua các lớp và phương thức liên quan đến Compose mà Android Studio đã tạo cho bạn.

Hàm có khả năng kết hợp

Hàm có khả năng kết hợp là hàm thông thường được chú thích bằng @Composable. Thao tác này cho phép hàm của bạn gọi các hàm @Composable khác trong đó. Bạn có thể thấy cách hàm Greeting được đánh dấu là @Composable. Hàm này sẽ tạo một phần của hệ phân cấp giao diện người dùng trình bày dữ liệu đầu vào đã cho (String). Text là hàm có khả năng kết hợp do thư viện cung cấp.

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

Compose trong ứng dụng Android

Trong Compose, Activity vẫn là điểm bắt đầu của ứng dụng Android. Trong dự án của chúng ta, MainActivity được khởi chạy khi người dùng mở ứng dụng (như chỉ định trong tệp AndroidManifest.xml). Bạn sẽ sử dụng setContent để xác định bố cục, nhưng thay vì sử dụng tệp XML như thường làm trong hệ thống View truyền thống, hãy gọi các hàm có khả năng kết hợp trong đó.

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.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme là cách định kiểu cho các hàm có khả năng kết hợp. Bạn sẽ thấy nội dung này trong phần Tạo giao diện cho ứng dụng. Để xem cách văn bản hiển thị trên màn hình, bạn có thể chạy ứng dụng trong trình mô phỏng hoặc thiết bị, hoặc sử dụng bản xem trước trên Android Studio.

Để sử dụng bản xem trước trên Android Studio, bạn chỉ cần đánh dấu mọi hàm có khả năng kết hợp không có tham số, hoặc các hàm có tham số mặc định bằng chú thích @Preview rồi tạo dự án. Bạn có thể thấy hàm Preview Composable trong tệp MainActivity.kt. Bạn có thể cho nhiều bản xem trước vào cùng một tệp và đặt tên cho các bản xem trước đó.

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

fb011e374b98ccff.png

Bản xem trước có thể không xuất hiện nếu bạn đã chọn eeacd000622ba9b.png. Hãy nhấp vào biểu tượng Phân tách 7093def1e32785b2.png để xem bản xem trước.

4. Chỉnh sửa giao diện người dùng

Hãy bắt đầu bằng cách đặt màu nền khác cho Greeting. Bạn có thể thực hiện việc này bằng cách gói thành phần kết hợp Text với Surface. Surface có màu nên vui lòng sử dụng MaterialTheme.colorScheme.primary.

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

Các thành phần lồng bên trong Surface sẽ được vẽ trên màu nền đó.

Bạn có thể thấy các thay đổi mới trong bản xem trước:

c88121ec49bde8c7.png

Có thể bạn đã bỏ lỡ một chi tiết quan trọng: văn bản đang có màu trắng. Bạn có nhớ đã đặt màu này khi nào không?

Sự thật là: chúng ta chưa chọn màu cho văn bản! Thành phần Material (chẳng hạn như androidx.compose.material3.Surface) được xây dựng để mang lại trải nghiệm tốt hơn bằng cách xử lý các tính năng phổ biến mà bạn có thể muốn dùng trong ứng dụng (chẳng hạn như chọn màu thích hợp cho văn bản). Material được gọi là một ngôn ngữ được định sẵn vì nó cung cấp các giá trị mặc định và mẫu hình phù hợp cho hầu hết ứng dụng. Thành phần Material trong Compose được xây dựng dựa trên các thành phần cơ bản khác (trong androidx.compose.foundation). Bạn cũng có thể truy cập vào đó qua thành phần ứng dụng trong trường hợp cần tăng sự linh hoạt.

Trong trường hợp này, Surface hiểu rằng khi nền được đặt thành màu primary, mọi văn bản phía trên nền đều sử dụng màu onPrimary. Màu này cũng được xác định trong giao diện. Bạn có thể tìm hiểu thêm về điều này trong phần Tạo giao diện cho ứng dụng.

Đối tượng sửa đổi

Hầu hết các thành phần trên giao diện người dùng trong Compose như SurfaceText đều chấp nhận tham số modifier (không bắt buộc). Đối tượng sửa đổi cho thành phần trên giao diện người dùng biết cách bố trí, hiển thị hoặc hoạt động trong bố cục mẹ. Bạn có thể nhận thấy thành phần kết hợp Greeting đã có đối tượng sửa đổi mặc định, sau đó được truyền đến Text.

Chẳng hạn như đối tượng sửa đổi padding sẽ áp dụng khoảng không gian xung quanh phần tử mà nó trang trí. Bạn có thể tạo một đối tượng sửa đổi khoảng đệm với Modifier.padding(). Bạn cũng có thể thêm nhiều đối tượng sửa đổi bằng cách tạo chuỗi để trong trường hợp này, chúng ta có thể thêm đối tượng sửa đổi khoảng đệm vào đối tượng sửa đổi mặc định: modifier.padding(24.dp).

Giờ thì hãy thêm khoảng đệm vào Text của bạn trên màn hình:

import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

ef14f7c54ae7edf.png

Nhiều đối tượng sửa đổi có thể dùng để căn chỉnh, tạo ảnh động, bố trí, tạo thao tác nhấp hoặc cuộn được, biến đổi, v.v. Để xem danh sách đầy đủ, hãy truy cập vào Danh sách đối tượng sửa đổi trong Compose. Bạn sẽ sử dụng một vài công cụ trong số đó ở các bước tiếp theo.

5. Sử dụng lại thành phần kết hợp

Bạn càng thêm nhiều thành phần vào giao diện người dùng thì càng tạo nhiều cấp độ lồng nhau. Điều này có thể ảnh hưởng đến khả năng đọc nếu một hàm thực sự trở nên lớn hơn. Khi tạo các thành phần nhỏ sử dụng lại được, bạn có thể dễ dàng xây dựng thư viện thành phần trên giao diện người dùng có trong ứng dụng. Mỗi phần này chịu trách nhiệm cho một phần của màn hình, ngoài ra còn có thể được chỉnh sửa độc lập.

Tốt nhất là hàm của bạn nên bao gồm một tham số Đối tượng sửa đổi được chỉ định một Đối tượng sửa đổi trống theo mặc định. Sau đó, hãy chuyển tiếp đối tượng sửa đổi này đến thành phần kết hợp đầu tiên mà bạn gọi bên trong hàm đó. Bằng cách này, trang web gọi có thể điều chỉnh hướng dẫn và hành vi về bố cục từ bên ngoài hàm có khả năng kết hợp.

Tạo một Thành phần kết hợp có tên là MyApp bao gồm cả lời chào.

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

Điều này cho phép bạn dọn dẹp lệnh gọi lại onCreate và bản xem trước, vì giờ đây bạn có thể sử dụng lại thành phần kết hợp MyApp để tránh việc trùng lặp mã.

Trong bản xem trước này, hãy gọi MyApp và xoá tên của bản xem trước.

Tệp MainActivity.kt của bạn sẽ có dạng như bên dưới:

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.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.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. Tạo các cột và hàng

Ba thành phần bố cục chuẩn cơ bản trong Compose là Column, RowBox.

518dbfad23ee1b05.png

Đây là các hàm có khả năng kết hợp và nhận tham số nội dung có thể kết hợp. Vì vậy, bạn có thể đặt các thành phần bên trong những hàm này. Ví dụ: mỗi thành phần con bên trong một Column sẽ được đặt theo chiều dọc.

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

Bây giờ, hãy thay đổi Greeting để làm hiển thị một cột có 2 thành phần văn bản, như trong ví dụ bên dưới:

bf27ee688c3231df.png

Lưu ý là bạn có thể phải di chuyển khoảng đệm xung quanh.

So sánh kết quả với giải pháp này:

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

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(modifier = modifier.padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Compose và Kotlin

Bạn có thể dùng các hàm có khả năng kết hợp như bất kỳ hàm nào khác trong Kotlin. Việc này giúp giao diện người dùng của ứng dụng trở nên thực sự hiệu quả vì bạn có thể thêm câu lệnh để tác động đến cách hiển thị giao diện người dùng.

Ví dụ: bạn có thể sử dụng vòng lặp for để thêm các thành phần vào Column:

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

a7ba2a8cb7a7d79d.png

Bạn chưa thiết lập kích thước hoặc thêm bất kỳ quy tắc ràng buộc nào vào kích thước của các thành phần kết hợp. Vì vậy, mỗi hàng sẽ chiếm không gian tối thiểu, đồng thời khả năng xem trước cũng tương tự như vậy. Hãy thay đổi bản xem trước để mô phỏng chiều rộng phổ biến của một chiếc điện thoại nhỏ là 320 dp. Thêm tham số widthDp vào chú thích @Preview như bên dưới:

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

a5d5f6cdbdd918a2.png

Công cụ sửa đổi được sử dụng rộng rãi trong Compose. Vì vậy, hãy thực hành một bài tập nâng cao hơn, đó là Sao chép bố cục bên dưới bằng công cụ sửa đổi fillMaxWidthpadding.

a9599061cf49a214.png

Giờ hãy so sánh mã của mình với giải pháp:

import androidx.compose.foundation.layout.fillMaxWidth

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

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Lưu ý:

  • Đối tượng sửa đổi có thể có quá nhiều thông tin, chẳng hạn như bạn có thể chỉ định nhiều cách để tạo khoảng đệm.
  • Để thêm nhiều đối tượng sửa đổi vào một phần tử, bạn chỉ cần liên kết chúng với nhau.

Có nhiều cách để đạt được kết quả này. Vậy nên, nếu mã của bạn không khớp với đoạn mã này, điều đó không có nghĩa là mã của bạn không đúng. Tuy nhiên, hãy sao chép và dán mã này để tiếp tục lớp học lập trình.

Thêm một nút

Ở bước tiếp theo, bạn sẽ thêm một phần tử nhấp vào được để mở rộng Greeting, vì vậy trước tiên chúng ta cần thêm nút đó. Mục tiêu là tạo bố cục bên dưới:

ff2d8c3c1349a891.png

Button là một thành phần kết hợp do gói material3 cung cấp, lấy thành phần kết hợp làm đối số cuối cùng. Do bạn có thể di chuyển biểu thức trailing lambda (lambda theo sau) ra ngoài dấu ngoặc đơn nên bạn cũng có thể thêm nội dung bất kỳ vào nút này làm thành phần con. Ví dụ như Text:

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

Để làm được việc này, bạn cần tìm hiểu cách đặt thành phần kết hợp ở cuối hàng. Không có đối tượng sửa đổi alignEnd. Thay vào đó, hãy cung cấp một số weight cho thành phần kết hợp ngay từ đầu. Đối tượng sửa đổi weight giúp thành phần lấp đầy tất cả không gian có sẵn, giúp nó trở nên linh hoạt và đẩy hiệu quả các thành phần khác không có trọng số – thành phần không linh hoạt. Điều này cũng khiến cho phương thức sửa đổi fillMaxWidth không còn cần thiết.

Giờ bạn hãy thử thêm nút này và đặt nó như trong hình trước.

Vui lòng xem giải pháp tại đây:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Trạng thái trong Compose

Ở phần này, bạn sẽ thêm một số hoạt động tương tác vào màn hình. Bạn hiện đã tạo được bố cục tĩnh, nhưng cần làm cho chúng phản ứng với những thay đổi của người dùng để đạt được điều này:

6675d41779cac69.gif

Trước khi tìm hiểu cách lập trình để nút có thể nhấp được và cách thay đổi kích thước mục, bạn cần lưu trữ một số giá trị ở đâu đó để cho biết từng mục có được mở rộng hay không – trạng thái của mục. Vì chúng ta cần sử dụng một trong các giá trị này cho mỗi lời chào, nên vị trí logic của hàm này sẽ nằm trong thành phần kết hợp Greeting. Vui lòng xem boolean expanded và cách sử dụng trong boolean này:

// Don't copy over
@Composable
fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

Vui lòng lưu ý chúng ta cũng đã thêm một hành động onClick và một văn bản cho nút động. Chúng ta sẽ nói thêm về điều này ở phần sau.

Tuy nhiên, tính năng này sẽ không hoạt động như mong đợi. Việc đặt một giá trị khác cho biến expanded sẽ không làm cho Compose phát hiện biến này là một thay đổi về trạng thái, nên sẽ không có thay đổi nào xảy ra.

Lý do việc thay đổi biến này không kích hoạt tính năng kết hợp lại là vì Compose không theo dõi biến. Ngoài ra, mỗi lần Greeting được gọi, biến sẽ được đặt lại thành giá trị false.

Để thêm trạng thái nội bộ vào một thành phần kết hợp, bạn có thể sử dụng hàm mutableStateOf. Hàm này sẽ khiến Compose kết hợp lại các hàm đọc State đó.

import androidx.compose.runtime.mutableStateOf
// ...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

Tuy nhiên, bạn không thể chỉ địnhmutableStateOf cho một biến bên trong thành phần kết hợp. Như đã giải thích trước đây, quá trình kết hợp lại có thể diễn ra bất cứ lúc nào nên sẽ gọi lại thành phần kết hợp, đặt lại trạng thái về trạng thái có thể thay đổi mới với giá trị là false.

Để giữ nguyên trạng thái qua các lần kết hợp lại, hãy nhớ trạng thái có thể thay đổi bằng cách sử dụng remember.

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

@Composable
fun Greeting(...) {
    val expanded = remember { mutableStateOf(false) }
    // ...
}

remember dùng để bảo vệ khỏi quá trình kết hợp lại, theo đó trạng thái không được đặt lại.

Lưu ý: nếu gọi cùng một thành phần kết hợp qua nhiều phần của màn hình thì bạn sẽ tạo ra nhiều thành phần trên giao diện người dùng, mỗi thành phần có phiên bản trạng thái riêng. Bạn có thể xem trạng thái nội bộ là một biến riêng tư trong một lớp.

Hàm có khả năng kết hợp sẽ tự động được "đăng ký" theo trạng thái. Nếu trạng thái thay đổi thì thành phần kết hợp đọc các trường này sẽ được kết hợp lại để cho thấy nội dung cập nhật.

Trạng thái tự biến đổi và phản ứng với các thay đổi về trạng thái

Khi thay đổi trạng thái, bạn có thể nhận thấy Button có một tham số được gọi là onClick nhưng không nhận giá trị mà chỉ nhận hàm.

Bạn có thể xác định hành động cần thực hiện khi nhấp bằng cách chỉ định biểu thức lambda cho hành động đó. Ví dụ: hãy chuyển đổi giá trị của trạng thái mở rộng và trình bày một văn bản khác tuỳ thuộc vào giá trị.

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

Chạy ứng dụng ở chế độ tương tác để xem hành vi.

374998ad358bf8d6.png

Khi người dùng nhấp vào nút này, expanded sẽ được bật/tắt để kích hoạt quá trình kết hợp lại văn bản bên trong nút đó. Mỗi Greeting duy trì một trạng thái mở rộng riêng, vì chúng thuộc các thành phần khác nhau trên giao diện người dùng.

93d839b53b7d9bea.gif

Mã cho đến thời điểm này:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Mở rộng mục

Giờ thì hãy mở rộng một mục khi được yêu cầu. Thêm một biến bổ sung phụ thuộc vào trạng thái của chúng ta:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...

Bạn không cần nhớ extraPadding khi kết hợp lại vì thuộc tính này đang thực hiện một phép tính đơn giản.

Và giờ thì chúng ta đã có thể áp dụng đối tượng sửa đổi khoảng đệm mới cho Cột:

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

Nếu chạy trên trình mô phỏng hoặc ở chế độ tương tác, bạn sẽ thấy mỗi mục có thể mở rộng một cách độc lập:

6675d41779cac69.gif

8. Chuyển trạng thái lên trên

Trong các hàm có khả năng kết hợp, trạng thái do nhiều hàm đọc hoặc sửa đổi phải nằm trong một đối tượng cấp trên chung — quy trình này được gọi là chuyển trạng thái lên trên. Đưa lên có nghĩa là nâng hoặc nâng lên.

Việc nâng cấp trạng thái lên trên sẽ tránh được tình trạng trùng lặp trạng thái và tạo ra lỗi, giúp tái sử dụng các thành phần kết hợp cũng như kết hợp kiểm thử dễ dàng hơn. Ngược lại, trạng thái không cần sự kiểm soát của cha mẹ một thành phần kết hợp sẽ không được nâng cấp lên trên. Nguồn đáng tin cậy thuộc về bất kỳ thành phần nào tạo ra và kiểm soát trạng thái đó.

Ví dụ: hãy tạo một màn hình giới thiệu cho ứng dụng.

5d5f44508fcfa779.png

Thêm mã sau vào MainActivity.kt:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

Mã này chứa một loạt các tính năng mới:

  • Bạn đã thêm một thành phần kết hợp mới có tên là OnboardingScreen và một bản xem trước mới. Nếu tạo dự án, bạn sẽ thấy có thể tồn tại nhiều bản xem trước cùng lúc. Chúng tôi cũng thêm chiều cao cố định để xác minh nội dung được căn chỉnh chính xác.
  • Column có thể được định cấu hình để hiển thị nội dung của ứng dụng ở giữa màn hình.
  • shouldShowOnboarding đang sử dụng từ khoá by thay vì =. Đây là uỷ quyền thuộc tính giúp bạn không phải nhập .value mỗi lần.
  • Khi bạn nhấp vào nút này, shouldShowOnboarding được đặt thành false. Tuy nhiên, bạn chưa đọc trạng thái này ở bất cứ đâu.

Bây giờ, chúng ta có thể thêm màn hình giới thiệu mới này vào ứng dụng của mình. Chúng tôi muốn màn hình hiển thị khi khởi chạy, sau đó ẩn màn hình khi người dùng nhấn "Tiếp tục".

Trong mục Compose, bạn không được ẩn các thành phần trên giao diện người dùng. Thay vào đó, bạn chỉ không thêm chúng vào bố cục, để chúng sẽ không được thêm vào cây giao diện người dùng mà Compose tạo ra. Bạn thực hiện việc này bằng logic Kotlin với điều kiện đơn giản. Ví dụ như để hiện màn hình giới thiệu hoặc danh sách lời chào, bạn có thể làm như sau:

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

Tuy nhiên, chúng tôi không có quyền truy cập vào shouldShowOnboarding. Rõ ràng là chúng ta cần chia sẻ trạng thái (state) đã tạo trong OnboardingScreen với thành phần kết hợp MyApp.

Thay vì chia sẻ giá trị của trạng thái với thành phần mẹ, chúng ta chuyển trạng thái lên trên – chỉ cần chuyển trạng thái đó đến đối tượng cấp trên chung cần truy cập vào trạng thái đó.

Trước tiên, hãy di chuyển nội dung của MyApp vào thành phần kết hợp mới tên là Greetings. Đồng thời, hãy điều chỉnh bản xem trước để gọi phương thức Greetings:

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    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)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

Thêm bản xem trước cho thành phần kết hợp MyApp mới ở cấp cao nhất để chúng ta có thể kiểm thử hành vi của thành phần kết hợp đó:

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

Giờ thì hãy thêm logic dưới đây để cho thấy các màn hình khác nhau trong MyAppchuyển trạng thái lên trên

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}

Chúng ta cũng cần chia sẻ shouldShowOnboarding với màn hình giới thiệu, nhưng không truyền theo cách trực tiếp. Thay vì để OnboardingScreen thay đổi trạng thái của mình, bạn nên thông báo cho chúng tôi khi người dùng nhấp vào nút Tiếp tục.

Chúng ta bỏ qua các sự kiện bằng cách nào? Bằng cách chuyển các lệnh gọi lại xuống. Lệnh gọi lại là các hàm được truyền dưới dạng đối số đến các hàm khác và được thực thi khi sự kiện xảy ra.

Hãy thử thêm một tham số hàm vào màn hình giới thiệu được xác định là onContinueClicked: () -> Unit để bạn có thể thay đổi trạng thái từ MyApp.

Giải pháp:

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

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

}

Khi chuyển một hàm chứ không phải trạng thái sang OnboardingScreen, chúng ta có thể làm cho thành phần kết hợp này dễ tái sử dụng hơn và bảo vệ để các thành phần kết hợp khác không làm biến đổi trạng thái. Nhìn chung thì điều này giúp mọi việc trở nên đơn giản. Một ví dụ điển hình là việc phải chỉnh sửa bản xem trước màn hình giới thiệu để gọi OnboardingScreen ngay:

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

Việc chỉ định onContinueClicked cho biểu thức lambda trống có nghĩa là "không làm gì cả", đây là lựa chọn hoàn hảo để xem trước.

Thật tuyệt vời, trải nghiệm này ngày càng giống với một ứng dụng thực sự!

25915eb273a7ef49.gif

Trong thành phần kết hợp MyApp, chúng ta đã dùng uỷ quyền thuộc tính by lần đầu tiên để tránh sử dụng giá trị mỗi lần. Hãy sử dụng by thay vì = trong thành phần kết hợp Greeting cho thuộc tính expanded. Hãy nhớ thay đổi expanded từ val thành var.

Mã đầy đủ cho đến thời điểm hiện tại:

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
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.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.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.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    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(
    modifier: Modifier = Modifier,
    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
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

9. Tạo danh sách lazy về hiệu suất

Giờ chúng ta hãy làm cho danh sách tên trở nên thực tế hơn. Tính đến thời điểm này, bạn đã hiển thị 2 lời chào trong Column. Nhưng liệu nó có thể xử lý hàng nghìn yêu cầu như thế không?

Thay đổi giá trị danh sách mặc định trong các tham số Greetings để sử dụng một hàm khởi tạo danh sách khác, cho phép đặt kích thước danh sách và điền giá trị đó vào hàm lambda (ở đây, $it đại diện cho chỉ mục danh sách):

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

Thao tác này sẽ tạo ra 1000 lời chào, ngay cả những lời chào không phù hợp với màn hình. Rõ ràng là việc này không hiệu quả. Bạn có thể thử chạy trên trình mô phỏng (cảnh báo: mã này có thể khiến trình mô phỏng của bạn bị treo).

Để hiển thị một cột có thể cuộn, chúng ta sẽ sử dụng LazyColumn. LazyColumn chỉ cho thấy các mục hiển thị trên màn hình, giúp tăng hiệu suất khi hiển thị một danh sách lớn.

Trong cách sử dụng cơ bản, API LazyColumn cung cấp một phần tử items trong phạm vi của nó, nơi logic cho thấy từng mục riêng lẻ được viết như sau:

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...

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

284f925eb984fb56.gif

10. Trạng thái cố định

Ứng dụng của chúng ta gặp phải 2 vấn đề:

Duy trì trạng thái màn hình giới thiệu

Nếu bạn chạy ứng dụng trên một thiết bị, nhấp vào các nút rồi xoay, màn hình giới thiệu sẽ hiển thị lại. Hàm remember chỉ hoạt động khi thành phần kết hợp được giữ lại trong Cấu trúc. Khi bạn xoay, toàn bộ hoạt động sẽ được khởi động lại khiến tất cả trạng thái bị mất. Điều này cũng xảy ra với mọi thay đổi về cấu hình và khi quá trình bị gián đoạn.

Bạn có thể sử dụng rememberSaveable thay vì remember. Thao tác này sẽ lưu từng thay đổi về trạng thái của cấu hình (chẳng hạn như xoay vòng) cũng như quá trình bị gián đoạn.

Giờ thì hãy thay thế remember trong shouldShowOnboarding bằng rememberSaveable:

    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

Chạy, xoay, chuyển sang chế độ tối hoặc dừng quy trình này. Màn hình giới thiệu sẽ không hiển thị trừ phi bạn đã thoát khỏi ứng dụng trước đó.

Duy trì trạng thái mở rộng của các mục trong danh sách

Nếu bạn mở rộng một mục danh sách rồi sau đó cuộn danh sách cho đến khi mục đó nằm ngoài khung hiển thị hoặc xoay thiết bị rồi sau đó quay lại mục mở rộng, thì bạn sẽ thấy mục đó hiện đã trở về trạng thái ban đầu.

Giải pháp cho trường hợp này là sử dụng rememberSaveable cho trạng thái mở rộng:

   var expanded by rememberSaveable { mutableStateOf(false) }

Với khoảng 120 dòng mã tính đến hiện tại, bạn có thể hiển thị một danh sách cuộn các mục dài và hiệu suất, mỗi mục giữ trạng thái của riêng chúng. Ngoài ra, như bạn có thể thấy, ứng dụng có chế độ tối hoàn toàn chính xác mà không cần thêm dòng mã. Bạn sẽ tìm hiểu về giao diện này sau.

11. Tạo ảnh động cho danh sách

Trong Compose, có nhiều cách để tạo ảnh động cho giao diện người dùng: từ API cấp cao đối với ảnh động đơn giản, cho đến phương thức cấp thấp để kiểm soát hoàn toàn và chuyển đổi phức tạp. Bạn có thể đọc về chúng trong tài liệu này.

Trong phần này, bạn sẽ sử dụng một trong các API cấp thấp, nhưng đừng lo, chúng cũng có thể rất đơn giản. Hãy tạo ảnh động cho sự thay đổi về kích thước mà chúng ta đã triển khai:

9efa14ce118d3835.gif

Để làm được điều này, bạn cần sử dụng thành phần kết hợp animateDpAsState. Thành phần này trả về một đối tượng Trạng thái có value liên tục được ảnh động cập nhật cho đến khi hoàn tất. Thành phần này sẽ nhận một "giá trị nhắm mục tiêu" có kiểu là Dp.

Tạo một extraPadding ảnh động phụ thuộc vào trạng thái mở rộng.

import androidx.compose.animation.core.animateDpAsState

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

Chạy ứng dụng rồi dùng thử ảnh động.

animateDpAsState lấy một tham số animationSpec không bắt buộc cho phép bạn tuỳ chỉnh ảnh động. Hãy làm điều gì đó thú vị hơn, chẳng hạn như thêm một ảnh động dựa trên lực lò xo:

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { 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))

    // ...

    )
}

Lưu ý rằng chúng ta cũng cần phải đảm bảo khoảng đệm không bao giờ có giá trị âm, nếu không thì ứng dụng có thể gặp sự cố. Nó gây ra một lỗi ảnh động nhỏ mà chúng ta sẽ khắc phục sau trong phần Các bước hoàn thiện cuối cùng.

Tham số spring không lấy bất kỳ tham số nào liên quan đến thời gian. Thay vào đó, dữ liệu này sử dụng các đặc điểm vật lý (giảm dần và độ cứng) để làm cho ảnh động tự nhiên hơn. Chạy ứng dụng ngay để dùng thử ảnh động mới:

9efa14ce118d3835.gif

Mọi ảnh động được tạo bằng animate*AsState đều có thể gây gián đoạn. Tức là nếu giá trị mục tiêu thay đổi ở giữa ảnh động, thì animate*AsState sẽ khởi động lại ảnh động và trỏ đến giá trị mới. Việc gián đoạn trông có vẻ hoàn toàn tự nhiên với ảnh động dựa trên lực lò xo:

d5dbf92de69db775.gif

Nếu bạn muốn khám phá nhiều kiểu ảnh động, hãy thử tham số khác cho spring, các thông số kỹ thuật khác (như tween, repeatable) và các hàm khác (animateColorAsState) hoặc loại API ảnh động khác.

Mã đầy đủ cho phần này

package com.example.basicscodelab

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.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.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    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(
    modifier: Modifier = Modifier,
    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, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colorScheme.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)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

12. Định kiểu và tạo giao diện cho ứng dụng

Hiện tại, bạn vẫn chưa tạo kiểu cho bất kỳ thành phần kết hợp nào nhưng vẫn có một chế độ mặc định hợp lý, bao gồm cả khả năng hỗ trợ chế độ tối! Hãy cùng tìm hiểu về BasicsCodelabThemeMaterialTheme.

Nếu mở tệp ui/theme/Theme.kt, bạn sẽ thấy BasicsCodelabTheme sử dụng MaterialTheme trong quá trình triển khai:

// Do not copy
@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MaterialTheme là một hàm có khả năng kết hợp phản ánh các nguyên tắc định kiểu trong phần Thông số kỹ thuật của Material Design. Thông tin định kiểu đó chuyển xuống các thành phần bên trong content, giúp đọc thông tin để tự tạo kiểu. Trong giao diện người dùng, bạn đã sử dụng BasicsCodelabTheme như sau:

    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

BasicsCodelabTheme gói MaterialTheme nội bộ, nên MyApp được định kiểu bằng các thuộc tính được xác định trong giao diện. Từ bất kỳ thành phần kết hợp con cháu nào, bạn cũng có thể truy xuất 3 thuộc tính của MaterialTheme: colorScheme, typographyshapes. Sử dụng chúng để đặt kiểu tiêu đề cho một trong các Text của bạn:

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

Thành phần kết hợp Text trong ví dụ trên sẽ đặt một TextStyle mới. Bạn có thể tạo TextStyle cho mình hoặc truy xuất một kiểu do giao diện xác định bằng cách sử dụng MaterialTheme.typography và cách này được ưu tiên hơn. Hàm dựng này giúp bạn truy cập vào các kiểu văn bản do Material xác định, chẳng hạn như displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium, v.v. Trong ví dụ, bạn sử dụng kiểu headlineMedium được xác định trong giao diện.

Giờ thì hãy dựng để xem văn bản mới được tạo kiểu:

673955c38b076f1c.png

Nhìn chung, bạn nên giữ màu, hình dạng và kiểu phông chữ bên trong MaterialTheme. Ví dụ: chế độ tối sẽ khó triển khai nếu bạn mã hoá cứng màu, đồng thời việc này sẽ đòi hỏi rất nhiều thao tác dễ xảy ra lỗi cần khắc phục.

Tuy nhiên, đôi khi bạn cũng cần một chút sai lệch trong việc chọn màu và kiểu phông chữ. Trong những trường hợp đó, bạn nên sử dụng màu hoặc kiểu nền khác với màu/kiểu nền hiện có.

Để khắc phục điều này, bạn có thể sửa đổi một kiểu được xác định trước bằng cách sử dụng hàm copy. Làm cho số đó in đậm hơn:

import androidx.compose.ui.text.font.FontWeight
// ...
Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

Theo đó, nếu cần thay đổi bộ phông chữ hoặc bất kỳ thuộc tính nào khác của headlineMedium, thì bạn sẽ không phải lo lắng về độ lệch nhỏ.

Và đây là kết quả trong cửa sổ xem trước:

b33493882bda9419.png

Thiết lập bản xem trước ở chế độ tối

Hiện tại, bản xem trước của chúng ta chỉ hiển thị giao diện của ứng dụng ở chế độ sáng. Thêm chú thích @Preview bổ sung vào GreetingPreview bằng UI_MODE_NIGHT_YES:

import android.content.res.Configuration.UI_MODE_NIGHT_YES

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

Thao tác này sẽ thêm bản xem trước ở chế độ tối.

2c94dc7775d80166.png

Chỉnh sửa giao diện của ứng dụng

Bạn có thể tìm thấy mọi thứ liên quan đến giao diện hiện tại ở các tệp bên trong thư mục ui/theme. Ví dụ như màu mặc định mà chúng ta đã sử dụng đến thời điểm này được xác định trong Color.kt.

Hãy bắt đầu bằng cách xác định màu mới. Thêm các ảnh này vào Color.kt:

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

Giờ bạn hãy chỉ định chúng vào bảng màu của MaterialTheme trong Theme.kt:

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

Nếu bạn quay lại MainActivity.kt rồi làm mới bản xem trước, thì các màu sắc của bản xem trước sẽ không thực sự thay đổi! Lý do là theo mặc định, Bản xem trước sẽ sử dụng màu động. Bạn có thể xem logic để thêm màu động trong Theme.kt bằng cách sử dụng tham số boolean dynamicColor.

Để xem phiên bản không thích ứng của bảng phối màu, hãy chạy ứng dụng trên thiết bị có cấp độ API dưới 31 (tương ứng với Android S có màu thích ứng). Bạn sẽ thấy các màu mới:

493d754584574e91.png

Trong Theme.kt, hãy xác định bảng khung hiển thị cho các màu tối:

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Giờ đây khi chạy ứng dụng, chúng ta sẽ thấy các màu tối:

84d2a903ffa6d8df.png

Mã cuối cùng cho Theme.kt

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

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

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

13. Các bước hoàn thiện cuối cùng!

Ở bước này, bạn sẽ áp dụng kiến thức đã biết và tìm hiểu các khái niệm mới chỉ với một vài gợi ý. Bạn sẽ tạo:

8d24a786bfe1a8f2.gif

Thay thế nút bằng một biểu tượng

  • Sử dụng thành phần kết hợp IconButton với Icon con.
  • Sử dụng Icons.Filled.ExpandLessIcons.Filled.ExpandMore có sẵn trong cấu phần phần mềm material-icons-extended. Hãy thêm dòng sau vào các phần phụ thuộc trong tệp app/build.gradle.kts.
implementation("androidx.compose.material:material-icons-extended")
  • Chỉnh sửa khoảng đệm để khắc phục lỗi căn chỉnh.
  • Thêm nội dung mô tả cho khả năng hỗ trợ tiếp cận (xem phần "Sử dụng tài nguyên chuỗi" dưới đây).

Sử dụng tài nguyên chuỗi

Phải có nội dung mô tả cho phần "Hiện thêm" và "ẩn bớt", bạn có thể thêm nội dung mô tả này bằng một câu lệnh if đơn giản:

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

Tuy nhiên, chuỗi mã hoá cứng không phải là một phương pháp tối ưu và bạn nên lấy các chuỗi này từ tệp strings.xml.

Bạn có thể sử dụng "Extract string resource" (Trích xuất tài nguyên chuỗi) trên mỗi chuỗi, có trong "Context Action" (Thao tác theo ngữ cảnh) trong Android Studio để tự động thực hiện việc này.

Ngoài ra, bạn có thể mở app/src/res/values/strings.xml và thêm vào các tài nguyên sau:

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

Hiện thêm

Văn bản "Composem ipsum" xuất hiện rồi biến mất, dẫn đến sự thay đổi về kích thước của mỗi thẻ.

  • Thêm Text mới vào Cột bên trong Greeting hiển thị khi mục được mở rộng.
  • Xoá extraPadding và thay vào đó, hãy áp dụng đối tượng sửa đổi animateContentSize cho Row. Việc này sẽ tự động hoá quy trình tạo ảnh động, cũng là quy trình rất khó thực hiện theo cách thủ công. Ngoài ra, nó còn giúp bạn không cần phải coerceAtLeast.

Thêm độ cao và hình dạng

  • Bạn có thể dùng đối tượng sửa đổi shadow cùng với đối tượng sửa đổi clip để có được giao diện thẻ. Tuy nhiên, có một thành phần kết hợp Material có thể thực hiện chính xác việc đó là Card. Bạn có thể thay đổi màu của Card bằng cách gọi CardDefaults.cardColors và ghi đè màu bạn muốn thay đổi.

Mã cuối cùng

package com.example.basicscodelab

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.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier, color = MaterialTheme.colorScheme.background) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    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(
    modifier: Modifier = Modifier,
    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, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { 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.headlineMedium.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 = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

14. Xin chúc mừng

Xin chúc mừng! Bạn đã tìm hiểu các kiến thức cơ bản về Compose!

Giải pháp cho lớp học lập trình

Bạn có thể lấy đoạn mã cho giải pháp của lớp học lập trình này trên kho lưu trữ GitHub:

$ git clone https://github.com/android/codelab-android-compose

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp Zip:

Tiếp theo là gì?

Hãy tham khảo các lớp học lập trình khác trên Lộ trình học Compose.

Tài liệu đọc thêm