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, đồng thời 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".

Khi tạo các thành phần kết hợp nhỏ sử dụng lại được, bạn có thể dễ dàng xây dựng thư viện các 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.

Để được hỗ trợ thêm khi tham gia lớp học lập trình này, vui lòng xem các mã sau:

Đ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:

5dcc23167391e246.gif

Những gì bạn cần

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 rồi chọn Start a new Android Studio project (Bắt đầu với một dự án Android Studio mới) như sau:

f5980dbff6f0fb7c.jpeg

Nếu màn hình ở trên không xuất hiện, hãy chuyển đến File (Tệp) > New (Mới) > New Project (Dự án mới).

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

a67ba73a4f06b7ac.png

Nhấp vào Next (Tiếp theo) và định cấu hình dự án như bình thường, gọi dự án là "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 Compose Activity (Hoạt động trống trong Compose), 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.gradleapp/build.gradle 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.colors.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    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 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/googlecodelabs/android-compose-codelabs

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 hiển thị dữ liệu đầu vào đã cho, String. Text là một hàm có thể kết hợp do thư viện cung cấp.

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

Compose trong ứng dụng Android

Trong Compose, Activity vẫn là điểm truy cập vào ứng dụng Android. Trong dự án của chúng ta, MainActivity được chạy khi người dùng mở ứng dụng (như được 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.colors.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 Giao diện cho ứng dụng. Để xem cách hiển thị văn bản 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 và 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 DefaultPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

debde226026ae047.png

Bản xem trước có thể không xuất hiện nếu bạn đã chọn f66a8adcef249de5.png. Hãy nhấp vào biểu tượng Phân tách f3c0e2f3221dadcb.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.colors.primary.

@Composable
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.primary) {
        Text (text = "Hello $name!")
    }
}

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

Khi thêm mã đó vào dự án, bạn sẽ thấy nút Tạo và làm mới ở góc trên cùng bên phải trong Android Studio. Nhấn vào đó hoặc tạo dự án để xem các thay đổi mới trong bản xem trước.

9632f3ca76cbe115.png

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

8216bdbc85a6ba94.png

Bạn có thể đã bỏ lỡ một chi tiết quan trọng: văn bản hiện đang có màu trắng. Chúng ta đã đặt màu này khi nào nhỉ?

Bạn chưa chọn màu cho văn bản! Các thành phần Material, như androidx.compose.material.Surface chẳng hạn, đượ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, ví dụ như chọn màu thích hợp cho văn bản. Chúng tôi cho rằng Material được định sẵn vì nó cung cấp các giá trị mặc định và mẫu hình phổ biến đối với hầu hết các ứng dụng. Các thành phần Material trong Compose được xây dựng dựa trên 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 linh hoạt hơn.

Trong trường hợp này, Surface hiểu khi nền được đặt thành màu primary, mọi văn bản phía trên nền đều phải 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 Chủ đề của ứng dụng.

Công cụ 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). Công cụ sửa đổi cho phần tử 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ẹ.

Chẳng hạn như công cụ 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 công cụ sửa đổi khoảng đệm với Modifier.padding().

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.Modifier
import androidx.compose.ui.unit.dp
...

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

Hãy nhấp vào nút Tạo và làm mới để xem các thay đổi mới.

4241a60d72a08f0b.png

Nhiều công cụ sửa đổi có thể dùng để căn chỉnh, tạo hiệu ứng, bố trí, tạo những thao tác có thể nhấp hoặc cuộn, biến đổi, v.v. Để xem danh sách đầy đủ, hãy truy cập vào Danh sách công cụ 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 các 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ạ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
private fun MyApp() {
    Surface(color = MaterialTheme.colors.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ã. Tệp MainActivity.kt của bạn sẽ có dạng như bên dưới:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basicstep1.ui.theme.BasicsCodelabTheme

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

@Composable
private fun MyApp() {
    Surface(color = MaterialTheme.colors.background) {
        Greeting("Android")
    }
}

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

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

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ụ như 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ó hai thành phần văn bản, như trong ví dụ bên dưới:

e42bf870995a84d2.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
private fun Greeting(name: String) {
    Surface(color = MaterialTheme.colors.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. Điều này giúp giao diện người dùng của ứng dụng trở nên thực sự mạnh mẽ, 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ụ như bạn có thể sử dụng vòng lặp for để thêm các phần tử vào Column:

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

b6265492ef236d70.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 DefaultPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

8722ec524e694ba5.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.

fd7cb2daa600875.png

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

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

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

Lưu ý:

  • Phương thức 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 công cụ 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:

e74f07b36865a878.png

Button là một thành phần kết hợp do gói material 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 hàm trabdas lambda ra ngoài dấu ngoặc đơn nên bạn có thể thêm bất kỳ nội dung nào vào nút này làm phần tử 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ó công cụ sửa đổi alignEnd. Thay vào đó, bạn hãy cung cấp một số weight cho thành phần kết hợp ngay từ đầu. Công cụ sửa đổi weight giúp phần tử lấp đầy tất cả các không gian có sẵn, khiến phần tử này trở nên linh hoạt, đẩy hiệu quả các phần tử khác không có trọng số – phần tử 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.material.Button
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...

@Composable
private fun Greeting(name: String) {

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

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:

ae3c993d793aa843.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
private fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

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 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 tái cấu trúc 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, việc kết hợp lại có thể diễn ra bất cứ lúc nào 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 từ các phần khác nhau của màn hình, bạn sẽ tạo các thành phần trên giao diện người dùng khác nhau, 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ý" với trạng thái. Nếu trạng thái thay đổi, thành phần kết hợp đọc các trường này sẽ được kết hợp lại để hiển thị 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ị, nó chỉ nhận hàm mà thôi.

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ụ như hãy chuyển đổi giá trị của trạng thái mở rộng và hiển thị một văn bản khác tuỳ thuộc vào giá trị.

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

Nếu chạy ứng dụng trong trình mô phỏng, bạn có thể thấy khi người dùng nhấp vào nút này, expanded sẽ được kích hoạt chuyển đổi cho 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 giao diện người dùng khác nhau.

825dd6d6f98bff05.gif

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

@Composable
private fun Greeting(name: String) {
    val expanded = remember { mutableStateOf(false) }

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

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
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

Bạn không cần phải 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 công cụ sửa đổi khoảng đệm mới cho Cột:

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

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

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

8c0da5d9a631ba97.png

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

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Arrangement

...

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

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = { shouldShowOnboarding = false }
            ) {
                Text("Continue")
            }
        }
    }
}

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

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ư bên dưới:

// Don't copy yet
@Composable
fun MyApp() {
    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 phần tử 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 được gọi là Greetings:

@Composable
fun MyApp() {
     Greetings()
}

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

Giờ thì hãy thêm logic này để hiển thị các màn hình khác nhau trong MyApp, và chuyển trạng thái lên trên

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier
                    .padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

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. Nói chung là đ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à cách cần 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ự!

c8c6c011ec37fe84.gif

Mã đầy đủ cho đến nay:

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

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

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

@Composable
private fun Greeting(name: String) {

    val expanded = remember { mutableStateOf(false) }

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

    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

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

9. Tạo danh sách tải 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 trong hàm lambda (đây là $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ỉ hiển thị 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 hiển thị từng mục riêng lẻ được viết như bên dưới:

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

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

b9ffef51a5fbc8ca.gif

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

Ứng dụng tồn tại một vấn đề: 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 thì 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 Bố cụ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.

Bây giờ, hãy thay thế remember trong shouldShowOnboarding bằng 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ừ khi bạn đã thoát khỏi ứng dụng trước đó.

d7802a1acb90beba.gif

Bản minh hoạ cho thấy cách một thay đổi về cấu hình (chuyển sang chế độ tối) không hiển thị lại màn hình giới thiệu.

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ừ các API cấp cao đối với các ảnh động đơn giản, cho đến các 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 những thay đổi về kích thước mà chúng ta đã triển khai:

83bbc35a3bd4b1b2.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. Ngoài ra, hãy sử dụng uỷ quyền thuộc tính (từ khoá by):

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

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:

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    ...

    )
}

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:

c14f0b8f617d21eb.gif

Mọi hoạt ảnh được tạo với animate*AsState đều có thể gây gián đoạn. Điều này có nghĩa 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:

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

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.BasicsCodelabTheme

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

@Composable
fun MyApp() {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

@Composable
fun OnboardingScreen(onContinueClicked: () -> Unit) {

    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

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

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

@Composable
private fun Greeting(name: String) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            OutlinedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

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

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

Bạn hiệ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 bạn vẫn có một chế độ mặc định hợp lý, bao gồm cả các hỗ trợ cho chế độ tối! Hãy cùng tìm hiểu về BasicsCodelabThemeMaterialTheme.

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

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}

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

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 nào, bạn cũng có thể truy xuất ba thuộc tính của MaterialTheme: colors, 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.h4)
            }

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ư h1-h6, body1,body2, caption, subtitle1, v.v. Trong ví dụ, bạn sử dụng kiểu h4 đượ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:

471658bc17da5b67.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ụ như 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:

                Text(
                    text = name,
                    style = MaterialTheme.typography.h4.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 h4, 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:

3c9a6d5d0939c813.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. 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 LightColorPalette = lightColors(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

Nếu quay lại MainActivity.kt và làm mới bản xem trước, bạn sẽ thấy các màu mới:

358b92c429c4c579.png

Tuy nhiên, bạn chưa sửa đổi màu tối. Trước khi làm việc đó, hãy thiết lập bản xem trước cho nó. Thêm chú giải @Preview bổ sung vào DefaultPreview bằng UI_MODE_NIGHT_YES:

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

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

7cfcdecdeccaf627.png

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

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

Ứng dụng của chúng ta hiện đã được thiết kế với giao diện và phong cách riêng!

351d2a0ff94056d1.png

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

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable

private val DarkColorPalette = darkColors(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

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

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) {
        DarkColorPalette
    } else {
        LightColorPalette
    }

    MaterialTheme(
        colors = colors,
        typography = typography,
        shapes = shapes,
        content = content
    )
}

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

Ở bước này, bạn sẽ áp dụng những gì đã 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:

5dcc23167391e246.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.
implementation "androidx.compose.material:material-icons-extended:$compose_version"
  • Chỉnh sửa khoảng đệm để sửa lỗi căn chỉnh.
  • Thêm phần mô tả nội dung cho khả năng hỗ trợ tiếp cận (xem phần "Sử dụng tài nguyên chuỗi" bên dưới).

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 và 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 công cụ sửa đổi animateContentSize cho Row. Việc này sẽ tự động hoá quy trình tạo ảnh động, điều này 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 công cụ sửa đổi shadow cùng với phương thức sửa đổi clip để có được giao diện thẻ. Tuy nhiên, cũng có một thành phần kết hợp Material có thể thực hiện chính xác điều đó là Card.

Mã cuối cùng

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.R
import com.codelab.basics.ui.BasicsCodelabTheme

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

@Composable
private fun MyApp() {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

@Composable
private fun OnboardingScreen(onContinueClicked: () -> Unit) {
    Surface {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text("Welcome to the Basics Codelab!")
            Button(
                modifier = Modifier.padding(vertical = 24.dp),
                onClick = onContinueClicked
            ) {
                Text("Continue")
            }
        }
    }
}

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

@Composable
private fun Greeting(name: String) {
    Card(
        backgroundColor = MaterialTheme.colors.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by remember { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name,
                style = MaterialTheme.typography.h4.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }

            )
        }
    }
}

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

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

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 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/googlecodelabs/android-compose-codelabs

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ì?

Vui lòng 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