נתונים בהיקף מקומי באמצעות CompositionLocal

CompositionLocal הוא כלי להעברת נתונים למטה דרך Composition באופן סמלי. בדף הזה נסביר בפירוט מהו CompositionLocal, איך יוצרים CompositionLocal משלכם ואיך אפשר לדעת אם CompositionLocal הוא פתרון מתאים לתרחיש לדוגמה שלכם.

גאים להציג: CompositionLocal

בדרך כלל, במצב 'כתיבה', הנתונים יורדים דרך עץ UI כפרמטרים לכל פונקציה קומפוזבילית. כך אפשר ליצור תוכן קומפוזבילי או יחסי תלות מפורשים. עם זאת, האפשרות הזו יכולה להיות מסורבלת לגבי נתונים שמשמשים לעיתים קרובות ובאופן נרחב, כמו צבעים או סגנונות גופן. דוגמה:

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

כדי לתמוך בכך שלא יהיה צורך להעביר את הצבעים כתלות מפורשת של פרמטר את רוב התכנים הקומפוזביליים, כתיבת הצעות כתיבה CompositionLocal מאפשרת כדי ליצור אובייקטים בעלי שם ברמת עץ שיכולים לשמש כדרך מרומזת עוברים דרך עץ ממשק המשתמש.

בדרך כלל, רכיבי CompositionLocal מקבלים ערך בצומת מסוים של עץ ממשק המשתמש. הצאצאים הניתנים ליצירה של הפונקציה יכולים להשתמש בערך הזה בלי להצהיר על CompositionLocal כפרמטר בפונקציה הניתנת ליצירה.

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 של CompositionLocalProvider תקבל את הערך שסופק כשתיגשת לנכס current של CompositionLocal. כאשר סופק ערך חדש, התכונה 'פיתוח נייטיב' מורכבת מחדש מחלקים ביצירה שקוראים CompositionLocal.

לדוגמה, השדה CompositionLocal ב-LocalContentColor מכיל את צבע התוכן המועדף לשימוש בטקסט ובסמלי אייקונים, כדי להבטיח שהוא יהיה בניגוד לצבע הרקע הנוכחי. ב בדוגמה הבאה, 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")
}

איור 1. תצוגה מקדימה של ה-composable של CompositionLocalExample.

בדוגמה האחרונה, המכונות CompositionLocal היו בשימוש פנימי לפי חומרים קומפוזביליים. כדי לגשת לערך הנוכחי של 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 מתקיים.

בנוסף, יכול להיות שלא יהיה מקור ברור לאמת של התלות הזו, כי היא יכולה להשתנות בכל חלק של ה-Composition. לכן, ניפוי באגים באפליקציה כשמתרחשת בעיה יכול להיות מאתגר יותר, כי צריך לנווט למעלה ב-Composition כדי לראות איפה הוענק הערך current. כלים כמו Finder משתמשים בסביבת הפיתוח המשולבת (IDE) או בכלי לבדיקת פריסת הכתיבה מספקים מספיק מידע כדי לצמצם את הבעיה.

החלטה אם להשתמש ב-CompositionLocal

יש תנאים מסוימים שבהם CompositionLocal יכול להיות פתרון טוב לתרחיש לדוגמה שלכם:

ל-CompositionLocal צריך להיות ערך ברירת מחדל טוב. אם אין ערך ברירת מחדל, עליכם להבטיח שמפתח יתקשה מאוד להגיע למצב שבו לא יסופק ערך ל-CompositionLocal. אם לא מציינים ערך ברירת מחדל, זה עלול לגרום לבעיות ולתסכול במהלך היצירה או צופים בתצוגה מקדימה של תוכן קומפוזבילי שמשתמש ב-CompositionLocal תמיד נדרש לציין זאת באופן מפורש.

אין להשתמש ב-CompositionLocal לקונספט שלא נחשב ברמת העץ או ברמת היררכיית המשנה. כדאי להשתמש ב-CompositionLocal כשכל הצאצאים יכולים להשתמש בו, ולא רק חלק מהם.

אם תרחיש השימוש שלכם לא עומד בדרישות האלה, כדאי לעיין בקטע חלופות שכדאי לשקול לפני יצירת CompositionLocal.

דוגמה לשיטה לא מומלצת היא יצירה של CompositionLocal שכולל את ViewModel של מסך מסוים, כך שכל התכנים הקומפוזביליים במסך הזה מקבלים הפניה אל ViewModel כדי לבצע לוגיקה. שיטה זו לא מומלצת בגלל שלא כל התכנים הקומפוזביליים שמתחת לעץ ממשק משתמש מסוים צריכים לדעת ViewModel מומלץ להעביר לרכיבים הניתנים לשילוב רק את המידע שנחוץ להם, לפי התבנית שבה המצב זורם למטה והאירועים זורמים למעלה. הגישה הזו תעזור לכם להשתמש שוב ברכיבים האלה בקלות רבה יותר ולבדוק אותם בקלות.

יצירת CompositionLocal

יש שני ממשקי API ליצירת CompositionLocal:

  • compositionLocalOf: שינוי הערך שסופק במהלך הרכבת מחדש מבטל רק את התוכן שקורא את הערך שלו ב-current.

  • staticCompositionLocalOf: בניגוד ל-compositionLocalOf, Compose לא עוקב אחרי קריאות של staticCompositionLocalOf. שינוי הערך גורם ליצירה מחדש של כל פונקציית הלמה content שבה CompositionLocal מסופק, במקום רק במקומות שבהם הערך current נקרא ב-Composition.

אם סביר מאוד שהערך שצוין ב-CompositionLocal לא ישתנה, או שהוא אף פעם לא ישתנה, כדאי להשתמש ב-staticCompositionLocalOf כדי לשפר את הביצועים.

לדוגמה, מערכת העיצוב של אפליקציה עשויה להגדיר דרך ספציפית להבליט רכיבים מורכבים באמצעות צל של רכיב ממשק המשתמש. מאחר שהמודל הגבהים של האפליקציה צריכות להתפשט בעץ ממשק המשתמש, אנחנו משתמשים CompositionLocal מאחר שהערך של CompositionLocal נגזר באופן מותנה בהתאם לעיצוב של המערכת, אנחנו משתמשים ב-API של 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")
    }
}

הגישה הזו יכולה להתאים יותר לתרחישי שימוש מסוימים, כי היא מפרידה את הצאצא מהאבות הקדמונים המיידיים שלו. רכיבי Compose של אב נוטים להיות מורכבים יותר, כדי לאפשר רכיבי Compose גמישים יותר ברמה נמוכה יותר.

באופן דומה, ניתן להשתמש ב-@Composable בתוכן lambdas באותו אופן כדי לקבל אותם יתרונות:

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

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