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.
Xin 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 = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← 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
CompositionLocal
là chế độ giao diện Material sử dụng nâng cao.
MaterialTheme
là
một đối tượng cung cấp 3 thực thể CompositionLocal
– 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
Bản sáng tác. 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 LocalContext
CompositionLocal
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) }
Đang 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.
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.
Đang 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 đọccurrent
.staticCompositionLocalOf
: Không giống nhưcompositionLocalOf
, bản đọc của mộtstaticCompositionLocalOf
không được theo dõi bằng ứng dụng Compose. Việc thay đổi giá trị sẽ khiến toàn bộ lambdacontent
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 } } } }
Sử dụng 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() } }
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Cấu trúc của một giao diện trong Compose
- Sử dụng Thành phần hiển thị trong Compose
- Kotlin cho Jetpack Compose