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

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

مقدمه‌ای بر CompositionLocal

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

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

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

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

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

  • ترکیب‌بندی عبارت است از ثبت نمودار فراخوانی توابع ترکیب‌پذیر.
  • درخت واسط کاربر یا سلسله‌مراتب واسط کاربر درختی از LayoutNode است که توسط فرایند ترکیب‌بندی ساخته، به‌روز، و نگهداری می‌شود.

CompositionLocal چیزی است که زمینه Material در پشت صحنه استفاده می‌کند. ‫MaterialTheme شیئی است که سه نمونه CompositionLocal ارائه می‌دهد: colorScheme، typography، و shapes، که به شما امکان می‌دهد بعداً آن‌ها را در هر بخش فرزند «ترکیب‌بندی» بازیابی کنید. به‌طور خاص، این‌ها مشخصه‌های 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 به بخشی از «ترکیب‌بندی» محدود می‌شود، بنابراین می‌توانید مقادیر مختلفی را در سطوح مختلف درخت ارائه دهید. مقدار current یک CompositionLocal با نزدیک‌ترین مقدار ارائه‌شده توسط یک جد در آن بخش از «ترکیب‌بندی» مطابقت دارد.

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

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

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

پیش‌نمایش CompositionLocalExample قابل‌ساخت.
شکل ۱. پیش‌نمایش CompositionLocalExample قابل‌ساخت.

در مثال آخر، نمونه‌های CompositionLocal به‌طور داخلی توسط ترکیب‌پذیرهای Material استفاده شده‌اند. برای دسترسی به مقدار فعلی 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 باعث می‌شود استدلال درباره رفتار یک ترکیب‌پذیر سخت‌تر شود. As they create implicit dependencies, callers of composables that use them need to make sure that a value for every CompositionLocal is satisfied.

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

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

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

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

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

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

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

ایجاد CompositionLocal

دو میانای برنامه‌سازی کاربردی برای ایجاد CompositionLocal وجود دارد:

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

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

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

برای مثال، سیستم طراحی برنامه ممکن است در نحوه ارتقای ترکیب‌پذیرها بااستفاده از سایه برای مؤلفه واسط کاربر، نظر خاصی داشته باشد. ازآنجایی‌که ارتفاع‌های مختلف برای برنامه باید در سراسر درخت میانای کاربر منتشر شود، از CompositionLocal استفاده می‌کنیم. ازآنجایی‌که مقدار CompositionLocal به‌صورت شرطی براساس زمینه سیستم مشتق می‌شود، از میانای برنامه‌سازی کاربردی 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() }

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

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

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

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

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

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