داده های محدوده محلی با CompositionLocal

CompositionLocal ابزاری برای انتقال داده ها از طریق Composition به طور ضمنی است. در این صفحه، با جزئیات بیشتری یاد خواهید گرفت که CompositionLocal چیست، چگونه CompositionLocal خود را ایجاد کنید، و بدانید که آیا CompositionLocal راه حل خوبی برای موارد استفاده شما است یا خیر.

معرفی CompositionLocal

معمولاً در Compose، داده ها از طریق درخت UI به عنوان پارامترهای هر تابع قابل ترکیب به پایین جریان می یابد . این امر وابستگی های یک composable را واضح می کند. با این حال، این ممکن است برای داده‌هایی که اغلب و به طور گسترده استفاده می‌شوند، مانند رنگ‌ها یا سبک‌های تایپ، دشوار باشد. مثال زیر را ببینید:

@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
    )
}

برای پشتیبانی از عدم نیاز به انتقال رنگ‌ها به عنوان یک وابستگی صریح پارامتر به بیشتر ترکیب‌پذیرها، Compose CompositionLocal ارائه می‌کند که به شما امکان می‌دهد اشیاء نام‌گذاری شده با دامنه درختی ایجاد کنید که می‌تواند به عنوان یک روش ضمنی برای داشتن جریان داده از طریق درخت UI استفاده شود.

عناصر CompositionLocal معمولاً با یک مقدار در گره خاصی از درخت UI ارائه می شوند. این مقدار می تواند توسط نوادگان قابل ترکیب آن بدون اعلام CompositionLocal به عنوان یک پارامتر در تابع composable استفاده شود.

CompositionLocal چیزی است که تم Material در زیر هود استفاده می کند. MaterialTheme شی‌ای است که سه نمونه CompositionLocal را ارائه می‌کند: colorScheme ، typography و shapes ، که به شما امکان می‌دهد بعداً آنها را در هر قسمتی از Composition بازیابی کنید. به طور خاص، اینها ویژگی‌های LocalColorScheme ، LocalShapes و LocalTypography هستند که می‌توانید از طریق ویژگی‌های MaterialTheme colorScheme ، shapes و typography به آنها دسترسی داشته باشید.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colorScheme, 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.colorScheme.primary
    )
}

یک نمونه CompositionLocal به بخشی از Composition اختصاص داده شده است تا بتوانید مقادیر مختلفی را در سطوح مختلف درخت ارائه دهید. مقدار current یک CompositionLocal با نزدیکترین مقدار ارائه شده توسط یک اجداد در آن قسمت از Composition مطابقت دارد.

برای ارائه یک مقدار جدید به CompositionLocal ، از CompositionLocalProvider و تابع infix provides که یک کلید CompositionLocal را به یک value مرتبط می کند، استفاده کنید. لامبدای content CompositionLocalProvider هنگام دسترسی به ویژگی current CompositionLocal مقدار ارائه شده را دریافت می کند. وقتی مقدار جدیدی ارائه می‌شود، Compose قسمت‌هایی از Composition را که CompositionLocal می‌خوانند، دوباره ترکیب می‌کند.

به عنوان مثالی از این، LocalContentColor CompositionLocal حاوی رنگ محتوای ترجیحی مورد استفاده برای متن و نماد نگاری است تا از تضاد آن با رنگ پس‌زمینه فعلی اطمینان حاصل کند. در مثال زیر، CompositionLocalProvider برای ارائه مقادیر مختلف برای بخش‌های مختلف Composition استفاده می‌شود.

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default
        // This is to automatically make text and other content contrast to the background
        // correctly.
        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() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the error color now")
}

شکل 1. پیش نمایش CompositionLocalExample composable.

در آخرین مثال، نمونه‌های CompositionLocal به صورت داخلی توسط مواد composable استفاده می‌شوند. برای دسترسی به مقدار فعلی یک CompositionLocal ، از ویژگی current آن استفاده کنید. در مثال زیر، مقدار Context فعلی LocalContext CompositionLocal که معمولاً در برنامه‌های Android استفاده می‌شود، برای قالب‌بندی متن استفاده می‌شود:

@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)
}

ایجاد CompositionLocal خودتان

CompositionLocal ابزاری برای انتقال داده ها از طریق Composition به طور ضمنی است.

یکی دیگر از سیگنال‌های کلیدی برای استفاده از CompositionLocal زمانی است که پارامتر مقطعی است و لایه‌های میانی پیاده‌سازی نباید از وجود آن آگاه باشند ، زیرا آگاه کردن آن لایه‌های میانی، کاربرد ترکیب‌پذیر را محدود می‌کند. به عنوان مثال، درخواست مجوزهای Android توسط CompositionLocal در زیر هود انجام می شود. یک انتخابگر رسانه قابل ترکیب می‌تواند عملکرد جدیدی را برای دسترسی به محتوای محافظت‌شده با مجوز در دستگاه بدون تغییر API آن اضافه کند و از تماس‌گیرندگان انتخابگر رسانه بخواهد از این زمینه اضافه استفاده شده از محیط آگاه باشند.

با این حال، CompositionLocal همیشه بهترین راه حل نیست. ما استفاده بیش از حد CompositionLocal را منع می کنیم زیرا دارای برخی از جنبه های منفی است:

CompositionLocal رفتار یک composable را برای استدلال سخت تر می کند . همانطور که وابستگی های ضمنی ایجاد می کنند، فراخواننده های composable که از آنها استفاده می کنند باید مطمئن شوند که یک مقدار برای هر CompositionLocal برآورده شده است.

علاوه بر این، ممکن است هیچ منبع روشنی از حقیقت برای این وابستگی وجود نداشته باشد زیرا می تواند در هر بخشی از ترکیب جهش پیدا کند. بنابراین، اشکال‌زدایی برنامه در هنگام بروز مشکل می‌تواند چالش‌برانگیزتر باشد ، زیرا باید در Composition حرکت کنید تا ببینید مقدار current کجا ارائه شده است. ابزارهایی مانند Find usages در IDE یا Compose layout inspector اطلاعات کافی برای کاهش این مشکل ارائه می دهند.

تصمیم گیری در مورد استفاده از CompositionLocal

شرایط خاصی وجود دارد که می تواند CompositionLocal به یک راه حل خوب برای موارد استفاده شما تبدیل کند:

یک CompositionLocal باید مقدار پیش فرض خوبی داشته باشد . اگر مقدار پیش‌فرض وجود ندارد، باید تضمین کنید که برای یک توسعه‌دهنده بسیار دشوار است که در موقعیتی قرار بگیرد که در آن مقداری برای CompositionLocal ارائه نشده باشد. عدم ارائه یک مقدار پیش‌فرض می‌تواند باعث ایجاد مشکل و ناامیدی در هنگام ایجاد آزمایش یا پیش‌نمایش یک composable شود که از آن CompositionLocal استفاده می‌کند، همیشه باید به طور صریح ارائه شود.

از CompositionLocal برای مفاهیمی که به عنوان محدوده درختی یا زیر سلسله مراتبی تصور نمی شوند، اجتناب کنید . یک CompositionLocal زمانی معنا پیدا می‌کند که بتوان آن را به طور بالقوه توسط هر نسلی استفاده کرد، نه تعداد کمی از آنها.

اگر مورد استفاده شما این الزامات را برآورده نمی کند، قبل از ایجاد CompositionLocal بخش Alternatives to consider را بررسی کنید.

یک مثال از یک عمل بد، ایجاد یک CompositionLocal است که ViewModel یک صفحه خاص را نگه می‌دارد، به طوری که همه اجزای سازنده در آن صفحه می‌توانند برای اجرای منطقی به ViewModel ارجاع دهند. این یک روش بد است زیرا همه اجزای سازنده زیر یک درخت UI خاص نیازی به دانستن یک ViewModel ندارند. روش خوب این است که فقط اطلاعاتی را که آنها به ترکیب‌پذیرها نیاز دارند، با پیروی از الگویی که حالت پایین می‌آید و رویدادها به سمت بالا می‌روند، منتقل می‌کنند. این رویکرد باعث می‌شود که کامپوزیشن‌های شما قابل استفاده مجدد و آزمایش آسان‌تر باشند.

ایجاد CompositionLocal

دو API برای ایجاد CompositionLocal وجود دارد:

  • compositionLocalOf : تغییر مقدار ارائه شده در حین ترکیب مجدد، فقط محتوایی را که مقدار current آن را می‌خواند، بی اعتبار می‌کند.

  • staticCompositionLocalOf : برخلاف compositionLocalOf ، خواندن staticCompositionLocalOf توسط Compose ردیابی نمی‌شود. تغییر مقدار باعث می شود که کل content لامبدا که در آن CompositionLocal ارائه شده است، به جای مکان هایی که مقدار current در Composition خوانده می شود، دوباره ترکیب شود.

اگر مقدار ارائه شده به CompositionLocal بسیار بعید است که تغییر کند یا هرگز تغییر نخواهد کرد، از staticCompositionLocalOf برای دریافت مزایای عملکرد استفاده کنید.

به عنوان مثال، سیستم طراحی یک برنامه ممکن است به روشی که ترکیب‌پذیرها با استفاده از یک سایه برای مؤلفه UI بالا می‌روند، مورد ارزیابی قرار گیرد. از آنجایی که ارتفاعات مختلف برنامه باید در سراسر درخت رابط کاربری منتشر شود، از CompositionLocal استفاده می‌کنیم. از آنجایی که مقدار CompositionLocal به صورت مشروط بر اساس موضوع سیستم مشتق می شود، ما از compositionLocalOf API استفاده می کنیم:

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

ارائه مقادیر به CompositionLocal

Composible CompositionLocalProvider مقادیر را به نمونه های CompositionLocal برای سلسله مراتب داده شده متصل می کند . برای ارائه یک مقدار جدید به CompositionLocal ، از تابع provides infix استفاده کنید که یک کلید CompositionLocal را به صورت زیر به یک value مرتبط می کند:

// 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
            }
        }
    }
}

مصرف CompositionLocal

CompositionLocal.current مقدار ارائه شده توسط نزدیکترین CompositionLocalProvider را برمی گرداند که مقداری را برای آن CompositionLocal ارائه می کند:

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

جایگزین هایی که باید در نظر گرفته شوند

CompositionLocal ممکن است راه حلی بیش از حد برای برخی موارد استفاده باشد. اگر مورد استفاده شما معیارهای مشخص شده در بخش تصمیم گیری برای استفاده از CompositionLocal را برآورده نمی کند، احتمالاً راه حل دیگری برای مورد استفاده شما مناسب تر است.

پارامترهای صریح را پاس کنید

صریح بودن در مورد وابستگی های composable یک عادت خوب است. ما توصیه می کنیم که قطعات کامپوزیشن را فقط آنچه را که نیاز دارند ارسال کنید . برای تشویق جداسازی و استفاده مجدد از مواد ترکیب‌پذیر، هر ترکیب‌پذیر باید حداقل اطلاعات ممکن را در خود داشته باشد.

@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
}

وارونگی کنترل

راه دیگر برای جلوگیری از انتقال وابستگی‌های غیرضروری به یک کامپوزیت از طریق وارونگی کنترل است. به جای اینکه فرزند وابستگی برای اجرای برخی منطق داشته باشد، والد این کار را به جای آن انجام می دهد.

مثال زیر را ببینید که در آن یک نسل باید درخواست بارگیری برخی از داده ها را راه اندازی کند:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

بسته به مورد، MyDescendant ممکن است مسئولیت زیادی داشته باشد. همچنین، انتقال MyViewModel به عنوان یک وابستگی باعث می شود MyDescendant کمتر قابل استفاده مجدد باشد زیرا آنها اکنون با هم جفت شده اند. جایگزینی را در نظر بگیرید که وابستگی را به نسل منتقل نمی کند و از وارونگی اصول کنترل استفاده می کند که جد را مسئول اجرای منطق می کند:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

این رویکرد می تواند برای برخی موارد استفاده بهتر مناسب باشد زیرا کودک را از اجداد نزدیکش جدا می کند . ترکیب‌پذیرهای اجدادی به نفع داشتن ترکیب‌پذیرهای سطح پایین‌تر انعطاف‌پذیرتر، پیچیده‌تر می‌شوند.

به طور مشابه، @Composable content lambdas را می توان به همان روش برای دریافت مزایای مشابه استفاده کرد :

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}