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

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

مقدمه‌ای بر CompositionLocal

معمولاً در Compose، داده‌ها از طریق درخت رابط کاربری به عنوان پارامترهایی به هر تابع composable جریان می‌یابند . این امر وابستگی‌های یک 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
    )
}

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

عناصر CompositionLocal معمولاً در یک گره خاص از درخت رابط کاربری مقداری دارند. آن مقدار می‌تواند توسط فرزندان composable آن بدون تعریف 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 و تابع میانوند 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")
}

پیش‌نمایش Composable از CompositionLocalExample.
شکل ۱. پیش‌نمایش Composable از CompositionLocalExample .

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

@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 زمانی است که پارامتر متقاطع است و لایه‌های میانی پیاده‌سازی نباید از وجود آن آگاه باشند، زیرا آگاه کردن آن لایه‌های میانی، کاربرد composable را محدود می‌کند. به عنوان مثال، پرس‌وجو برای مجوزهای اندروید توسط CompositionLocal در زیر کاپوت ارائه می‌شود. یک media picker composable می‌تواند قابلیت‌های جدیدی را برای دسترسی به محتوای دارای مجوز محافظت‌شده در دستگاه بدون تغییر API آن و نیاز به فراخوانی‌کنندگان media picker برای آگاهی از این زمینه اضافه شده استفاده شده از محیط، اضافه کند.

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

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

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

تصمیم بگیرید که آیا از CompositionLocal استفاده کنید یا خیر

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

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

از CompositionLocal برای مفاهیمی که به عنوان tree-scope یا sub-hierarchy-scope در نظر گرفته نمی‌شوند، اجتناب کنید . یک CompositionLocal زمانی منطقی است که بتواند به طور بالقوه توسط هر فرزندی مورد استفاده قرار گیرد، نه توسط تعداد کمی از آنها.

اگر مورد استفاده شما این الزامات را برآورده نمی‌کند، قبل از ایجاد CompositionLocal بخش «جایگزین‌هایی که باید در نظر گرفته شوند» را بررسی کنید.

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

ایجاد یک CompositionLocal

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

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

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

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

برای مثال، سیستم طراحی یک برنامه ممکن است در نحوه‌ی ارتقای کامپوننت‌های ترکیبی با استفاده از سایه برای کامپوننت رابط کاربری، خودرأی باشد. از آنجایی که ارتقای مختلف برای برنامه باید در سراسر درخت رابط کاربری منتشر شود، ما از یک 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 ارائه دهید

Composable CompositionLocalProvider مقادیر را به نمونه‌های CompositionLocal برای سلسله مراتب داده شده متصل می‌کند . برای ارائه یک مقدار جدید به یک CompositionLocal ، از تابع میانوند provides استفاده کنید که یک کلید 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ها نیاز دارند، به آنها منتقل کنید . برای تشویق جداسازی و استفاده مجدد از composableها، هر 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، استفاده از وارونگی کنترل است. به جای اینکه فرزند، وابستگی را برای اجرای برخی منطق‌ها دریافت کند، والد این کار را انجام می‌دهد.

به مثال زیر توجه کنید که در آن یک فرزند باید درخواست بارگذاری برخی داده‌ها را آغاز کند:

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

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

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

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

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

{% کلمه به کلمه %} {% فعل کمکی %} {% کلمه به کلمه %} {% فعل کمکی %}