داده‌های محدوده محلی با 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
    )
}

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

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

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

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

CompositionLocal چیزی است که زمینه «ماتریال» در پشت صحنه از آن استفاده می‌کند. 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 مرتبط می‌کند استفاده کنید. وقتی به دارایی current در CompositionLocal دسترسی پیدا می‌کنید، لامبدای content در CompositionLocalProvider مقدار ارائه‌شده را دریافت خواهد کرد. وقتی مقدار جدیدی ارائه می‌شود، 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 ابزاری برای انتقال داده‌ها به‌صورت ضمنی ازطریق «ترکیب» است.

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

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

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

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

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

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

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

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

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

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

ایجاد CompositionLocal

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

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

  • staticCompositionLocalOf: برخلاف compositionLocalOf، خواندن staticCompositionLocalOf توسط «نوشتن» ردیابی نمی‌شود. تغییر مقدار باعث می‌شود کل لامبدای 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
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 می‌توان به همان روش برای دریافت مزایای یکسان استفاده کرد:

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

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