CompositionLocal ابزاری برای ارسال داده بهصورت ضمنی درون Composition است. در این صفحه یاد میگیرید CompositionLocal دقیقاً چیست، چگونه CompositionLocal مخصوص خودتان را بسازید، و چطور تشخیص دهید آیا CompositionLocal برای مورد استفاده شما راهحل مناسبی است یا نه.
معرفی CompositionLocal
معمولاً در Compose، دادهها از طریق درخت UI و بهصورت پارامتر به هر تابع Composable به سمت پایین جریان پیدا میکنند. این کار وابستگیهای یک Composable را واضح و صریح میکند. با این حال، برای دادههایی که بسیار زیاد و در بخشهای گستردهای استفاده میشوند، مانند رنگها یا styleهای نوشتار، این روش میتواند سخت و خستهکننده باشد. مثال زیر را ببینید:
@Composable
fun MyApp() {
// اطلاعات Theme معمولاً نزدیک ریشه اپلیکیشن تعریف میشود
val colors = colors()
}
// یک Composable در عمق سلسلهمراتب
@Composable
fun SomeTextLabel(labelText: String) {
Text(
text = labelText,
color = colors.onPrimary // ← اینجا باید به colors دسترسی داشته باشیم
)
}
برای اینکه لازم نباشد colors را بهعنوان یک وابستگی پارامتری صریح به بیشتر Composableها پاس دهید، Compose ابزاری به نام CompositionLocal ارائه میدهد. این ابزار به شما اجازه میدهد objectهای نامگذاریشده و محدود به یک درخت ایجاد کنید که بتوانند بهعنوان روشی ضمنی برای جریان یافتن داده در درخت UI استفاده شوند.
عناصر CompositionLocal معمولاً در یک نود مشخص از درخت UI یک مقدار دریافت میکنند. آن مقدار میتواند توسط Composableهای فرزند آن نود استفاده شود، بدون اینکه CompositionLocal بهعنوان پارامتر در تابع Composable اعلام شود.
اصطلاحات کلیدی:
در این راهنما از اصطلاحات Composition، UI tree و UI hierarchy استفاده میشود. اگرچه ممکن است در راهنماهای دیگر این اصطلاحات بهجای هم استفاده شوند، اما معنی متفاوتی دارند:
- Composition رکوردی از call graph مربوط به توابع Composable است.
- UI tree یا UI hierarchy درختی از
LayoutNodeها است که توسط فرایند Composition ساخته، بهروزرسانی و نگهداری میشود.
CompositionLocal همان چیزی است که Material theme در پشت صحنه از آن استفاده میکند. MaterialTheme یک object است که سه نمونه CompositionLocal ارائه میدهد: colorScheme، typography و shapes. این موارد به شما اجازه میدهند بعداً در هر بخش فرزند از Composition به آنها دسترسی پیدا کنید.
بهطور مشخص، اینها propertyهای LocalColorScheme، LocalShapes و LocalTypography هستند که میتوانید از طریق attributeهای colorScheme، shapes و typography در MaterialTheme به آنها دسترسی داشته باشید.
@Composable
fun MyApp() {
// یک Theme فراهم میکند که مقدارهای آن به content پایینتر منتقل میشوند
MaterialTheme {
// مقدارهای جدید colorScheme، typography و shapes
// در lambda مربوط به content در MaterialTheme در دسترس هستند.
// ... محتوا اینجا قرار میگیرد ...
}
}
// یک Composable در عمق سلسلهمراتب MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
Text(
text = labelText,
// primary از CompositionLocal مربوط به LocalColors در MaterialTheme گرفته میشود
color = MaterialTheme.colorScheme.primary
)
}
یک نمونه CompositionLocal به بخشی از Composition محدود است؛ بنابراین میتوانید در سطحهای مختلف درخت، مقدارهای متفاوتی ارائه دهید. مقدار current مربوط به یک CompositionLocal برابر با نزدیکترین مقداری است که توسط یک ancestor در همان بخش از Composition ارائه شده است.
برای ارائه یک مقدار جدید به CompositionLocal، از CompositionLocalProvider و تابع infix آن یعنی provides استفاده کنید. این تابع یک key مربوط به CompositionLocal را به یک value متصل میکند. lambda مربوط به content در CompositionLocalProvider هنگام دسترسی به property به نام current از CompositionLocal، مقدار ارائهشده را دریافت میکند. وقتی مقدار جدیدی ارائه شود، Compose بخشهایی از Composition را که آن CompositionLocal را خواندهاند دوباره Recomposition میکند.
برای نمونه، CompositionLocal به نام LocalContentColor رنگ پیشنهادی محتوا را برای متن و iconها نگه میدارد تا مطمئن شود با رنگ پسزمینه فعلی contrast مناسبی دارد. در مثال زیر، از CompositionLocalProvider برای ارائه مقدارهای مختلف در بخشهای متفاوتی از Composition استفاده شده است.
@Composable
fun CompositionLocalExample() {
MaterialTheme {
// Surface بهصورت پیشفرض contentColorFor(MaterialTheme.colorScheme.surface) را فراهم میکند
// این کار باعث میشود متن و سایر محتواها بهصورت خودکار
// contrast درستی با پسزمینه داشته باشند.
Surface {
Column {
Text("Uses Surface's provided content color")
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
Text("Primary color provided by LocalContentColor")
Text("This Text also uses primary as textColor")
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
DescendantExample()
}
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProviderها در میان توابع Composable نیز کار میکنند
Text("This Text uses the error color now")
}
CompositionLocal بهصورت داخلی توسط Composableهای Material استفاده شدند. برای دسترسی به مقدار فعلی یک CompositionLocal، از property آن به نام current استفاده کنید. در مثال زیر، مقدار فعلی Context از CompositionLocal به نام LocalContext که معمولاً در اپلیکیشنهای اندروید استفاده میشود، برای format کردن متن استفاده شده است:@Composable
fun FruitText(fruitSize: Int) {
// resources را از مقدار فعلی LocalContext دریافت میکند
val resources = LocalContext.current.resources
val fruitText = remember(resources, fruitSize) {
resources.getQuantityString(R.plurals.fruit_title, fruitSize)
}
Text(text = fruitText)
}
نکته:
objectها یا constantهای مربوط به CompositionLocal معمولاً با پیشوند Local نامگذاری میشوند تا با auto-complete در IDE راحتتر پیدا شوند.
ساخت CompositionLocal مخصوص خودتان
CompositionLocal ابزاری برای ارسال ضمنی داده به سمت پایین در Composition است.
نشانه مهم دیگر برای استفاده از CompositionLocal زمانی است که پارامتر، cross-cutting باشد و لایههای میانی پیادهسازی نباید از وجود آن آگاه باشند؛ چون آگاه کردن آن لایههای میانی میتواند کاربردپذیری Composable را محدود کند.
برای مثال، درخواست permissionهای اندروید در پشت صحنه با یک CompositionLocal فراهم میشود. یک Composable مربوط به media picker میتواند قابلیت جدیدی برای دسترسی به محتوای محافظتشده با permission روی دستگاه اضافه کند، بدون اینکه API آن تغییر کند و بدون اینکه callerهای media picker مجبور باشند از این context اضافهشده در محیط آگاه شوند.
با این حال، CompositionLocal همیشه بهترین راهحل نیست. استفاده بیش از حد از CompositionLocal توصیه نمیشود، چون چند نقطه ضعف دارد:
CompositionLocal باعث میشود فهمیدن رفتار یک Composable سختتر شود. چون وابستگیهای ضمنی ایجاد میکند، callerهای Composableهایی که از آن استفاده میکنند باید مطمئن شوند برای هر CompositionLocal مقدار مناسبی فراهم شده است.
علاوه بر این، ممکن است منبع حقیقت واضحی برای این وابستگی وجود نداشته باشد، چون میتواند در هر بخشی از Composition تغییر کند. بنابراین debug کردن اپلیکیشن هنگام بروز مشکل میتواند چالشبرانگیزتر باشد؛ چون باید در Composition به سمت بالا حرکت کنید تا ببینید مقدار فعلی یا current کجا ارائه شده است.
ابزارهایی مانند Find usages در IDE یا Compose layout inspector اطلاعات کافی برای کاهش این مشکل فراهم میکنند.
نکته:CompositionLocal برای معماریهای پایهای خوب عمل میکند و Jetpack Compose بهطور گسترده از آن استفاده میکند.
تصمیمگیری درباره استفاده از CompositionLocal
شرایطی وجود دارد که میتواند CompositionLocal را به راهحل خوبی برای مورد استفاده شما تبدیل کند:
یک CompositionLocal باید مقدار پیشفرض خوبی داشته باشد. اگر مقدار پیشفرض وجود ندارد، باید تضمین کنید که برای توسعهدهنده بسیار سخت باشد در موقعیتی قرار بگیرد که مقداری برای آن CompositionLocal ارائه نشده باشد. فراهم نکردن مقدار پیشفرض میتواند هنگام ساخت testها یا preview کردن Composableای که از آن CompositionLocal استفاده میکند مشکل و ناراحتی ایجاد کند، چون همیشه باید بهصورت صریح مقدار ارائه شود.
برای conceptهایی که tree-scoped یا sub-hierarchy scoped در نظر گرفته نمیشوند، از CompositionLocal استفاده نکنید. یک CompositionLocal زمانی منطقی است که بتواند بهصورت بالقوه توسط هر descendant استفاده شود، نه فقط توسط چند مورد محدود.
اگر مورد استفاده شما این نیازها را برآورده نمیکند، قبل از ساخت یک CompositionLocal، بخش راهحلهای جایگزین را بررسی کنید.
یک مثال از روش بد این است که یک CompositionLocal بسازید که ViewModel یک صفحه خاص را نگه دارد تا همه Composableهای آن صفحه بتوانند reference مربوط به ViewModel را دریافت کرده و بعضی منطقها را اجرا کنند. این روش بد است، چون همه Composableهای زیر یک درخت UI خاص لازم نیست درباره ViewModel بدانند.
روش درست این است که فقط اطلاعاتی را به Composableها پاس دهید که واقعاً به آن نیاز دارند، بر اساس الگویی که در آن State به پایین جریان پیدا میکند و Eventها به بالا میروند. این رویکرد باعث میشود Composableهای شما قابل استفاده مجددتر و تستپذیرتر شوند.
ساخت یک CompositionLocal
دو API برای ساخت CompositionLocal وجود دارد:
compositionLocalOf: تغییر مقداری که هنگام Recomposition ارائه شده، فقط محتوایی را invalid میکند که مقدارcurrentآن را میخواند.staticCompositionLocalOf: برخلافcompositionLocalOf، خواندن مقدارهایstaticCompositionLocalOfتوسط Compose دنبال نمیشود. تغییر مقدار باعث میشود کل lambda مربوط بهcontentکهCompositionLocalدر آن ارائه شده Recomposition شود، نه فقط جاهایی که مقدارcurrentدر Composition خوانده شده است.
اگر مقداری که به CompositionLocal ارائه میشود به احتمال زیاد تغییر نمیکند یا هرگز تغییر نخواهد کرد، از staticCompositionLocalOf استفاده کنید تا از مزایای performance بهره ببرید.
برای مثال، design system یک اپلیکیشن ممکن است دیدگاه مشخصی درباره نحوه elevation دادن به Composableها با استفاده از shadow برای componentهای UI داشته باشد. چون elevationهای مختلف اپلیکیشن باید در سراسر درخت UI منتشر شوند، از یک CompositionLocal استفاده میکنیم. از آنجا که مقدار CompositionLocal بهصورت شرطی بر اساس theme سیستم به دست میآید، از API به نام compositionLocalOf استفاده میکنیم:
// فایل LocalElevations.kt
data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)
// یک object عمومی CompositionLocal با مقدار پیشفرض تعریف میکند
// این instance میتواند توسط همه Composableهای اپلیکیشن استفاده شود
val LocalElevations = compositionLocalOf { Elevations() }
ارائه مقدارها به CompositionLocal
Composable به نام CompositionLocalProvider مقدارها را برای سلسلهمراتب مشخصشده به نمونههای CompositionLocal متصل میکند. برای ارائه یک مقدار جدید به CompositionLocal، از تابع infix به نام provides استفاده کنید که key مربوط به CompositionLocal را به یک value متصل میکند؛ مانند زیر:
// فایل MyActivity.kt
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// محاسبه elevationها بر اساس theme سیستم
val elevations = if (isSystemInDarkTheme()) {
Elevations(card = 1.dp, default = 1.dp)
} else {
Elevations(card = 0.dp, default = 0.dp)
}
// اتصال elevation بهعنوان مقدار LocalElevations
CompositionLocalProvider(LocalElevations provides elevations) {
// ... محتوا اینجا قرار میگیرد ...
// این بخش از Composition هنگام دسترسی به LocalElevations.current
// instance مربوط به elevations را میبیند
}
}
}
}
مصرف کردن CompositionLocal
CompositionLocal.current مقداری را برمیگرداند که توسط نزدیکترین CompositionLocalProvider ارائه شده و برای آن CompositionLocal مقدار فراهم کرده است:
@Composable
fun SomeComposable() {
// دسترسی به متغیر عمومی LocalElevations برای گرفتن
// Elevations فعلی در این بخش از Composition
MyCard(elevation = LocalElevations.current.card) {
// محتوا
}
}
راهحلهای جایگزین
CompositionLocal ممکن است برای بعضی موارد راهحلی بیش از حد بزرگ باشد. اگر مورد استفاده شما معیارهای گفتهشده در بخش تصمیمگیری درباره استفاده از CompositionLocal را ندارد، احتمالاً راهحل دیگری برای مورد استفاده شما مناسبتر است.
پاس دادن پارامترهای صریح
شفاف بودن درباره وابستگیهای Composable عادت خوبی است. توصیه میشود فقط چیزی را به Composableها پاس دهید که واقعاً به آن نیاز دارند. برای تقویت جداسازی و استفاده مجدد از Composableها، هر Composable باید کمترین مقدار اطلاعات ممکن را نگه دارد.
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
MyDescendant(myViewModel.data)
}
// کل object را پاس ندهید! فقط چیزی را پاس دهید که descendant نیاز دارد.
// همچنین ViewModel را بهعنوان یک وابستگی ضمنی با CompositionLocal پاس ندهید.
@Composable
fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }
// فقط چیزی را پاس دهید که descendant لازم دارد
@Composable
fun MyDescendant(data: DataToDisplay) {
// نمایش داده
}
Inversion of control
راه دیگر برای جلوگیری از پاس دادن وابستگیهای غیرضروری به یک Composable استفاده از inversion of control است. بهجای اینکه descendant یک وابستگی دریافت کند تا بعضی منطقها را اجرا کند، parent این کار را انجام میدهد.
مثال زیر را ببینید که در آن یک descendant باید درخواست load کردن مقداری data را trigger کند:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
MyDescendant(myViewModel)
}
@Composable
fun MyDescendant(myViewModel: MyViewModel) {
Button(onClick = { myViewModel.loadData() }) {
Text("Load data")
}
}
بسته به مورد، MyDescendant ممکن است مسئولیت زیادی داشته باشد. همچنین پاس دادن MyViewModel بهعنوان وابستگی باعث میشود MyDescendant کمتر قابل استفاده مجدد باشد، چون این دو به هم وابسته میشوند.
راهحل جایگزینی را در نظر بگیرید که وابستگی را به descendant پاس نمیدهد و از اصول inversion of control استفاده میکند؛ بهطوری که ancestor مسئول اجرای منطق میشود:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
ReusableLoadDataButton(
onLoadClick = {
myViewModel.loadData()
}
)
}
@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
Button(onClick = onLoadClick) {
Text("Load data")
}
}
این رویکرد میتواند برای بعضی موارد مناسبتر باشد، چون child را از ancestorهای مستقیم خودش جدا میکند. Composableهای ancestor معمولاً پیچیدهتر میشوند، اما در عوض Composableهای سطح پایینتر انعطافپذیرتر خواهند شد.
به شکل مشابه، lambdaهای مربوط به @Composable content نیز میتوانند به همین روش استفاده شوند تا همان مزایا به دست آید:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
ReusablePartOfTheScreen(
content = {
Button(
onClick = {
myViewModel.loadData()
}
) {
Text("Confirm")
}
}
)
}
@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
Column {
// ...
content()
}
}
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: منبع مستندات