Phân cấp kiến trúc của Jetpack Compose

Trang này cung cấp thông tin tổng quan cấp cao về các phân cấp kiến trúc tạo nên Jetpack Compose cũng như các nguyên tắc cốt lõi ảnh hưởng đến cách sắp xếp này.

Jetpack Compose không phải một khối dự án đơn lẻ; thư viện này được tạo nên từ nhiều mô-đun lắp ráp với nhau nhằm tạo thành một ngăn xếp hoàn chỉnh. Hiểu rõ được các mô-đun cấu thành nên Jetpack Compose sẽ giúp bạn:

  • Sử dụng mức độ trừu tượng phù hợp để tạo nên ứng dụng hoặc thư viện
  • Hiểu được khi nào bạn cần "hạ cấp" để có thêm quyền kiểm soát hoặc tuỳ chỉnh
  • Giảm thiểu các phần phụ thuộc

Các phân cấp

Các cấp chính của Jetpack Compose bao gồm:

Hình 1. Các phân cấp chính của Jetpack Compose.

Mỗi cấp được xây dựng dựa trên các phân cấp thấp hơn, phối hợp cùng các tính năng để tạo nên các đối tượng trên cấp cao hơn. Mỗi cấp được xây dựng dựa trên các API công khai của các cấp thấp hơn để xác định các giới hạn mô-đun và cho phép bạn thay thế bất kỳ phân cấp nào nếu cần thiết. Hãy cùng tìm hiểu các phân cấp này từ dưới lên.

Thời gian chạy
Mô-đun này cung cấp những thành phần cơ bản về thời gian chạy của Compose, chẳng hạn như remember, mutableStateOf, chú giải @ComposableSideEffect. Bạn có thể xem xét xây dựng trực tiếp trên phân cấp này nếu chỉ sử dụng khả năng quản lý sơ đồ cây của Compose mà không cần đến giao diện người dùng (UI).
Giao diện người dùng (UI)
Phân cấp giao diện người dùng (UI) gồm nhiều mô-đun (ui-text, ui-graphics, ui-tooling, v.v.). Các mô-đun này triển khai các thành phần cơ bản của bộ công cụ giao diện người dùng (UI), chẳng hạn như LayoutNode, Modifier, trình xử lý nhập liệu (input handler), bố cục tuỳ chỉnh (custom layout) và bản vẽ (drawing). Bạn có thể xem xét xây dựng trên phân cấp này khi chỉ cần đến các thuộc tính cơ bản của bộ công cụ giao diện người dùng (UI).
Nền tảng
Mô-đun này cung cấp các thành phần độc lập của hệ thống thiết kế cho giao diện người dùng của Compose, chẳng hạn như Row, ColumnLazyColumn, ghi nhận các cử chỉ cụ thể, v.v. Bạn có thể cân nhắc việc dựng trên lớp nền tảng để tạo hệ thống thiết kế cho riêng mình.
Material
Mô-đun này cung cấp quá trình triển khai của hệ thống Material Design cho giao diện người dùng (UI) Compose, cung cấp một hệ thống chủ đề, tạo kiểu cho các thành phần, các chỉ báo gợn sóng và biểu tượng. Hãy xây dựng trên phân cấp này khi bạn sử dụng Material Design trong ứng dụng.

Nguyên tắc thiết kế

Nguyên tắc chung của Jetpack Compose là cung cấp các nhóm chức năng tập trung nhỏ, có thể được lắp ráp (hoặc gộp lại) với nhau, thay vì nhiều khối thành phần. Cách tiếp cận này có một số ưu điểm.

Kiểm soát

Các đối tượng trên phân cấp cao hơn có xu hướng thực hiện nhiều tác vụ hơn cho bạn, nhưng chúng sẽ hạn chế các quyền kiểm soát trực tiếp của bạn. Nếu cần kiểm soát nhiều hơn, bạn có thể "thả xuống" để sử dụng một thành phần cấp thấp hơn.

Ví dụ: nếu muốn tạo hiệu ứng chuyển màu của một thành phần nào đó, bạn có thể sử dụng API animateColorAsState:

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

Tuy nhiên, nếu cần thành phần này luôn bắt đầu hiệu ứng với màu xám thì bạn không thể sử dụng API này. Thay vào đó, bạn có thể hạ cấp xuống để sử dụng API Animatable ở phân cấp thấp hơn:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

API animateColorAsState trên phân cấp cao hơn được xây dựng dựa trên API Animatable ở phân cấp thấp hơn. Việc sử dụng API cấp thấp tuy phức tạp hơn nhưng mang lại quyền kiểm soát cao hơn. Hãy chọn mức độ trừu tượng phù hợp nhất với nhu cầu của bạn.

Tuỳ chỉnh

Việc tập hợp các thành phần cấp cao hơn từ các thành phần cấp thấp hơn làm cho việc tuỳ chỉnh các thành phần trở nên dễ dàng hơn nhiều. Ví dụ: hãy xem xét cách triển khai cho đối tượng Button do phân cấp Material cung cấp:

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Đối tượng Button được tạo nên từ 4 thành phần:

  1. Thành phần Surface về chất liệu cung cấp nền, hình dạng, xử lý lượt nhấp, v.v.

  2. Phương thức CompositionLocalProvider thay đổi độ đậm nhạt của nội dung khi đối tượng nút được kích hoạt hoặc vô hiệu

  3. Phương thức ProvideTextStyle thiết lập kiểu văn bản mặc định

  4. Phương thức Row cung cấp nguyên tắc bố cục mặc định cho nội dung của đối tượng nút đó

Chúng tôi đã xoá một số tham số và chú giải để khiến cấu trúc rõ ràng hơn, dù vậy toàn bộ thành phần chỉ có khoảng 40 dòng lệnh bởi nó đơn giản chỉ lắp ráp lại 4 thành phần này để triển khai đối tượng nút. Các thành phần như đối tượng Button sẽ hiển thị các tham số cố định, cân bằng các tuỳ chỉnh được cấp phép phổ biến để tránh các trường hợp dùng quá nhiều tham số, khiến thành phần đó trở nên khó sử dụng hơn. Ví dụ: các thành phần Material sẽ cung cấp các tuỳ chỉnh cụ thể trong hệ thống Material Design, giúp bạn dễ dàng tuân theo các nguyên tắc thiết kế Material.

Tuy nhiên, nếu bạn muốn tuỳ chỉnh các thuộc tính khác của một tham số thành phần thì bạn có thể hạ cấp và phát triển thành phần đó trên một nhánh khác. Ví dụ: Material Design quy định các đối tượng nút phải có màu đồng nhất. Nếu bạn muốn đối tượng nút có màu biến đổi tuyến tính (gradient), thuộc tính này sẽ không hỗ trợ cho tham số của đối tượng Button này. Trong trường hợp đó, bạn có thể sử dụng phương thức triển khai đối tượng Button của phân cấp Material để tham chiếu cho đối tượng của riêng mình:

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

Cách triển khai phương thức trên tiếp tục sử dụng các thành phần từ cấp Material, chẳng hạn như thuộc tính Material về độ đậm nhạt của phần nội dung hiện tại và kiểu văn bản đang dùng. Tuy nhiên, cách triển khai này sẽ thay thế material của phương thức Surface bằng Row và tạo kiểu để đối tượng đạt được hình thức như mong muốn.

Nếu bạn hoàn toàn không muốn sử dụng các thuộc tính chung của Material, chẳng hạn như khi xây dựng hệ thống thiết kế cho riêng mình, bạn có thể hạ cấp để sử dụng các thành phần trên phân cấp nền tảng:

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

Jetpack Compose sử dụng cách đặt tên đơn giản nhất cho các thành phần trên phân cấp cao nhất. Ví dụ: androidx.compose.material.Text được tạo dựa trên androidx.compose.foundation.text.BasicText. Điều này giúp bạn có thể tiến hành quá trình triển khai những cái tên dễ nhận biết nhất, trong trường hợp bạn có nhu cầu thay đổi các phân cấp cao hơn.

Chọn mức độ trừu tượng phù hợp

Theo quan niệm của Compose khi xây dựng các phân cấp chồng lên nhau, các thành phần có thể tái sử dụng đồng nghĩa với việc bạn không phải lúc nào cũng cần tới các thành phần ở phân cấp thấp hơn. Các thành phần trên cấp cao hơn không chỉ cung cấp thêm nhiều tính năng mà còn luôn triển khai các phương thức xử lý tốt nhất như hỗ trợ khả năng tiếp cận.

Ví dụ: nếu bạn muốn thêm tính năng hỗ trợ bằng cử chỉ vào thành phần tuỳ chỉnh, bạn có thể tạo tính năng này từ đầu bằng cách sử dụng thành phần Modifier.pointerInput, nhưng vẫn có các thành phần khác ở cấp cao hơn được dựng trên thành phần này có thể có điểm xuất phát tốt hơn, ví dụ: Modifier.draggable, Modifier.scrollable hoặc Modifier.swipeable.

Theo nguyên tắc, hãy ưu tiên xây dựng các thành phần trên phân cấp cao nhất bởi chúng cung cấp các tính năng giúp bạn hưởng lợi từ những phương thức xử lý tốt nhất mà phân cấp đó có.

Tìm hiểu thêm

Hãy tham khảo Mẫu Jetsnack để xem ví dụ về cách xây dựng hệ thống thiết kế tuỳ chỉnh.