Dữ liệu trong phạm vi cục bộ với CompositionLocal

Dữ liệu trong phạm vi cục bộ với CompositionLocal

Composition là thuốc gì

CompositionLocal là một công cụ giúp truyền dữ liệu thông qua Cấu phần (Composition) một cách ngầm ẩn. Trên trang này, bạn sẽ tìm hiểu chi tiết hơn về CompositionLocal, cách tạo CompositionLocal của riêng mình và liệu CompositionLocal có phải là giải pháp hiệu quả cho trường hợp sử dụng của bạn hay không.

Giới thiệu CompositionLocal

Thông thường trong Compose, dữ liệu sẽ chạy theo luồng thông qua cây giao diện người dùng dưới dạng tham số cho từng hàm có khả năng kết hợp. Điều này giúp các phần phụ thuộc của thành phần kết hợp (composable) trở nên rõ ràng. Tuy nhiên, việc này có thể cồng kềnh đối với dữ liệu rất thường xuyên và được sử dụng rộng rãi như màu sắc hoặc kiểu loại. Hãy xem ví dụ sau:

@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = … } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = // ← need to access colors here ) }

Để hỗ trợ việc không cần truyền các màu dưới dạng phần phụ thuộc tham số rõ ràng đến hầu hết thành phần kết hợp, Compose cung cấp CompositionLocal cho phép bạn tạo các đối tượng có tên trong phạm vi cây có thể được dùng như một cách ngầm ẩn để truyền dữ liệu qua cây giao diện người dùng.

Các thành phần CompositionLocal thường được cung cấp với một giá trị ở một nút nhất định trong cây giao diện người dùng. Giá trị con có thể kết hợp của hàm đó có thể sử dụng giá trị đó mà không cần khai báo CompositionLocal dưới dạng một tham số trong hàm có khả năng kết hợp

Thuật ngữ quan trọng: Trong hướng dẫn này, chúng tôi sử dụng các thuật ngữ Composition, Cây giao diện người dùngHệ phân cấp giao diện người dùng. Mặc dù chúng có thể được sử dụng cả trong các hướng dẫn khác nhưng ý nghĩa sẽ khác nhau.

Cấu phần (Composition) là bản ghi biểu đồ lệnh gọi của các hàm có khả năng kết hợp.

Cây giao diện người dùng hay Hệ phân cấp giao diện người dùng là cây hệ thống LayoutNode được tạo, cập nhật và duy trì theo quy trình cấu trúc.

CompositionLocal là chế độ giao diện Material sử dụng nâng cao. MaterialThemelà đối tượng cung cấp ba phiên bảnCompositionLocal -màu sắc, kiểu chữ và hình dạng – cho phép bạn truy xuất chúng sau này trong bất kỳ phần con nào của Cấu phần. Cụ thể, đây là các thuộc tính LocalColors, LocalShapes và LocalTypography mà bạn có thể truy cập thông qua các thuộc tính MaterialTheme colors, shapes và typography.

@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colors, typography, and shapes are available // in MaterialTheme’s content lambda. // … content here … } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme’s // LocalColors CompositionLocal color = MaterialTheme.colors.primary ) }

Một thực thể CompositionLocal chỉ thuộc một phần của Cấu phần để bạn có thể cung cấp nhiều giá trị ở nhiều cấp trên cây. Giá trị current của CompositionLocal tương ứng với giá trị gần nhất mà đối tượng cấp trên trong Cấu phần đó cung cấp.

Để cung cấp giá trị mới cho CompositionLocal, hãy sử dụng CompositionLocalProvider và hàm infixprovides mà liên kết khoá CompositionLocal với value. Labda content của CompositionLocalProvider sẽ nhận được giá trị đã cung cấp khi truy cập vào thuộc tính current của CompositionLocal. Khi một giá trị mới được cung cấp, tính năng Compose sẽ kết hợp lại các phần của Cấu phần có đọc CompositionLocal.

Ví dụ, LocalContentAlpha CompositionLocal chứa nội dung ưu tiên alpha được sử dụng cho văn bản và biểu tượng để nhấn mạnh hoặc xem nhẹ các phần khác nhau của giao diện người dùng. Trong ví dụ sau, CompositionLocalProvider được dùng để cung cấp các giá trị khác nhau cho các phần khác nhau của Cấu phần.

@Composable fun CompositionLocalExample() { MaterialTheme { // MaterialTheme sets ContentAlpha.high as default Column { Text(“Uses MaterialTheme’s provided alpha”) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Text(“Medium value provided for LocalContentAlpha”) Text(“This Text also uses the medium value”) CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) { DescendantExample() } } } } } @Composable fun DescendantExample() { // CompositionLocalProviders also work across composable functions Text(“This Text uses the disabled alpha now”) }

Hình 1. Bản xem trước thành phần kết hợp CompositionLocalExample.

Trong tất cả ví dụ ở trên, các phiên bản CompositionLocal đều được sử dụng trong nội bộ bởi các thành phần kết hợp Material. Để truy cập giá trị hiện tại của CompositionLocal, hãy sử dụng thuộc tính current. Trong ví dụ sau, giá trị Context hiện tại của LocalContextCompositionLocal thường dùng trong các ứng dụng Android sẽ được dùng để định dạng văn bản:

@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) } Lưu ý: Các đối tượng hoặc hằng số CompositionLocal thường được thêm tiền tố Local để cho phép cải thiện khả năng khám phá nhờ tính năng tự động hoàn tất trong IDE.

Tạo CompositionLocal của riêng bạn

CompositionLocal là một công cụ để truyền dữ liệu xuống thông qua Cấu phần một cách ngầm ẩn.

Một tín hiệu quan trọng khác để sử dụng CompositionLocal là khi tham số bị cắt chéo và các lớp triển khai trung gian không được biết rằng có sự tồn tại, vì việc làm cho các lớp trung gian đó biết được sẽ hạn chế phần mềm tiện ích của thành phần kết hợp. Ví dụ: truy vấn cho các quyền của Android được cung cấp bởi CompositionLocal nâng cao. Một thành phần kết hợp của công cụ chọn phương tiện có thể thêm chức năng mới để truy cập nội dung được bảo vệ bằng quyền trên thiết bị mà không cần thay đổi API và yêu cầu phương thức gọi công cụ chọn phương tiện phải biết ngữ cảnh bổ sung được sử dụng trong môi trường.

Tuy nhiên, CompositionLocal không phải lúc nào cũng là giải pháp tốt nhất. Chúng tôi không khuyến khích lạm dụng CompositionLocal vì nó có một số nhược điểm:

CompositionLocal khiến hành vi của thành phần kết hợp khó giải thích hơn. Khi tạo ra các phần phụ thuộc ngầm ẩn, trình gọi của các thành phần kết hợp mà sử dụng các phần phụ thuộc đó cần đảm bảo rằng một giá trị cho mỗi CompositionLocal đều được đáp ứng.

Hơn nữa, có thể không có nguồn thông tin rõ ràng về phần phụ thuộc này vì phần phụ thuộc này có thể thay đổi trong bất kỳ phần nào của Cấu phần. Do đó, việc gỡ lỗi ứng dụng khi xảy ra sự cố có thể khó khăn hơn vì bạn cần phải di chuyển lên Cấu phần để xem giá trị current được cung cấp ở đâu. Các công cụ như Tìm thông tin sử dụng trong IDE hoặc Layout Inspector của Compose cung cấp đủ thông tin để giảm thiểu vấn đề này.

Lưu ý: CompositionLocal hoạt động tốt cho cấu trúc cơ bản và Jetpack Compose tận dụng nó rất nhiều.

Quyết định có sử dụng CompositionLocal hay không

Có một số điều kiện nhất định có thể khiến CompositionLocal trở thành giải pháp hiệu quả cho trường hợp sử dụng của bạn:

CompositionLocal phải có giá trị mặc định tốt. Nếu không có giá trị mặc định, bạn phải đảm bảo rằng nhà phát triển gặp khó khăn quá nhiều khi gặp phải tình huống mà giá trị cho CompositionLocal không được cung cấp. Việc không cung cấp giá trị mặc định có thể gây ra sự cố và thất vọng khi tạo các thử nghiệm hoặc xem trước một thành phần kết hợp mà sử dụng CompositionLocal sẽ luôn yêu cầu phải được cung cấp rõ ràng.

Tránh sử dụng CompositionLocal cho các khái niệm không được coi là áp dụng cho phạm vi cây hoặc thứ bậc. CompositionLocal là hợp lý khi nó có thể được sử dụng bởi bất kỳ thành phần con nào, chứ không phải một vài trong số đó.

Nếu trường hợp sử dụng của bạn không đáp ứng các yêu cầu này, hãy xem phần Phương án thay thế cần xem xét trước khi tạo CompositionLocal.

Ví dụ về một phương pháp không nên áp dụng là tạo một CompositionLocal chứa ViewModel của một màn hình cụ thể sao cho mọi thành phần kết hợp trong màn hình đó đều có thể tham chiếu đến ViewModel để triển khai một số logic. Đây là một phương pháp không nên áp dụng vì không phải tất cả thành phần kết hợp dưới một cây giao diện người dùng cụ thể đều cần biết về ViewModel. Cách tốt nhất là chỉ truyền cho các thành phần kết hợp thông tin mà họ cần theo mẫu mà trạng thái giảm xuống và các sự kiện tăng lên. Cách tiếp cận này sẽ giúp các thành phần kết hợp của bạn có thể sử dụng lại và thử nghiệm dễ dàng hơn.

Tạo một CompositionLocal

Có hai API để tạo một CompositionLocal:

  • compositionLocalOf: Việc thay đổi giá trị đã cung cấp trong quá trình soạn lại sẽ chỉ làm mất hiệu lực nội dung đọc current.

  • staticCompositionLocalOf: Không giống như compositionLocalOf, bản đọc của một staticCompositionLocalOf không được theo dõi bằng ứng dụng Compose. Việc thay đổi giá trị sẽ khiến toàn bộ lambda content mà CompositionLocal được cung cấp được kết hợp lại, thay vì chỉ áp dụng cho các vị trí mà giá trị current được đọc trong Thành phần.

Nếu giá trị bạn cung cấp cho CompositionLocal có nhiều khả năng không thay đổi hoặc không bao giờ thay đổi, hãy sử dụng staticCompositionLocalOf để nhận các lợi ích hiệu suất.

Ví dụ: hệ thống thiết kế của ứng dụng có thể giữ nguyên cách nâng cao các thành phần kết hợp bằng cách sử dụng hiệu ứng đổ bóng cho thành phần giao diện người dùng. Vì có nhiều độ nâng (elevation) của ứng dụng phải áp dụng trong toàn bộ cây giao diện người dùng, chúng tôi sử dụng CompositionLocal. Vì giá trị CompositionLocal được lấy theo điều kiện dựa trên giao diện hệ thống, chúng tôi sử dụng API compositionLocalOf:

// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }

Cung cấp giá trị cho CompositionLocal

Thành phần kết hợp CompositionLocalProvider liên kết các giá trị với phiên bản CompositionLocal cho Hệ phân cấp đã có. Để cung cấp giá trị mới cho CompositionLocal, hãy sử dụng hàm sửa lỗi infix provides liên kết khoá CompositionLocal với value như sau:

// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // … Content goes here … // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }

Tiêu thụ CompositionLocal

CompositionLocal.current trả về giá trị do CompositionLocalProvider gần nhất cung cấp một giá trị cho CompositionLocal đó:

@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition Card(elevation = LocalElevations.current.card) { // Content } }

Lựa chọn thay thế đáng cân nhắc

CompositionLocal có thể là một giải pháp quá mức cho một số trường hợp sử dụng. Nếu trường hợp sử dụng của bạn không đáp ứng các tiêu chí quy định trong mục Quyết định có sử dụng CompositionLocal hay không, thì có một giải pháp khác có thể sẽ phù hợp hơn với trường hợp sử dụng của bạn.

Truyền các tham số tường minh

Bạn nên trình bày rõ ràng các phần phụ thuộc của thành phần kết hợp . Bạn nên truyền cho ác thành phần kết hợp chỉ những gì chúng cần. Để khuyến khích việc phân tách và sử dụng lại thành phần kết hợp, mỗi thành phần kết hợp phải chứa ít thông tin nhất có thể.

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // … MyDescendant(myViewModel.data) } // Don’t pass the whole object! Just what the descendant needs. // Also, don’t pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { … } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }

Đảo ngược quyền kiểm soát

Một cách khác để tránh truyền các phần phụ thuộc không cần thiết đến một thành phần kết hợp là thông qua đảo ngược quyền kiểm soát. Thay vì thành phần con nhận một phần phụ thuộc để thực thi một logic, thành phần gốc nên làm điều đó.

Xem ví dụ sau đây, trong đó thành phần con cần kích hoạt yêu cầu để tải một số dữ liệu:

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // … MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text(“Load data”) } }

Tuỳ thuộc vào trường hợp, MyDescendant có thể phải chịu nhiều trách nhiệm. Ngoài ra, việc truyền MyViewModel dưới dạng phần phụ thuộc làm cho MyDescendant có thể sử dụng lại ít hơn vì các phần này hiện đã được kết hợp với nhau. Hãy xem xét phương án thay thế không truyền phần phụ thuộc vào thành phần con và sử dụng quy tắc kiểm soát đảo ngược khiến đối tượng cấp trên phải chịu trách nhiệm thực thi logic:

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // … ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text(“Load data”) } }

Phương pháp này có thể phù hợp hơn cho một số trường hợp sử dụng vì nó lấy thành phần con khỏi đối tượng cấp trên trực tiếp. Các thành phần kết hợp của đối tượng cấp trên có xu hướng trở nên phức tạp hơn thay vì có nhiều hơn các thành phần kết hợp có tính linh hoạt ở cấp độ thấp hơn.

Tương tự, bạn có thể sử dụng @Composable nội dung lambda theo cách tương tự để nhận được các lợi ích tương tự:

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // … ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text(“Confirm”) } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // … content() } }