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

CompositionLocal הוא כלי להעברת נתונים באופן מרומז דרך הרכיב Composition. בדף הזה מוסבר בפירוט מהו 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 כפרמטר בפונקציה קומפוזבילית.

מונחים חשובים: במדריך הזה אנחנו משתמשים במונחים Composition,‏ UI tree ו-UI hierarchy. למרות שאולי משתמשים בהם לסירוגין במדריכים אחרים, יש להם משמעויות שונות:

  • הקומפוזיציה היא רשומה של גרף השיחות של פונקציות שאפשר להרכיב.
  • עץ ממשק המשתמש או היררכיית ממשק המשתמש הוא עץ של LayoutNode שנבנה, מתעדכן ומתוחזק על ידי תהליך הקומפוזיציה.

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

כדי לספק ערך חדש ל-CompositionLocal, משתמשים בפונקציה CompositionLocalProvider ובפונקציה provides infix שמשייכת מפתח CompositionLocal ל-value. פונקציית ה-lambda‏ content של 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")
}

תצוגה מקדימה של רכיב ה-Composable‏ CompositionLocalExample.
איור 1. תצוגה מקדימה של רכיב ה-CompositionLocalExample.

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

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

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

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

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

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

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

דוגמה לשיטה לא מומלצת היא יצירה של CompositionLocal שמכיל את ViewModel של מסך מסוים, כך שכל הרכיבים הקומפוזביליים במסך הזה יכולים לקבל הפניה אל ViewModel כדי לבצע לוגיקה מסוימת. זו שיטה לא מומלצת כי לא כל הרכיבים הקומפוזביליים שמתחת לעץ מסוים של ממשק המשתמש צריכים לדעת על ViewModel. השיטה המומלצת היא להעביר לרכיבים הניתנים להרכבה רק את המידע שהם צריכים, לפי הדפוס state flows down and events flow up (הסטטוס זורם למטה והאירועים זורמים למעלה). הגישה הזו תאפשר לכם לעשות שימוש חוזר ברכיבים הניתנים להרכבה ולבדוק אותם בקלות רבה יותר.

צור CompositionLocal

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

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

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

אם הערך שצוין ל-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, יכול להיות שפתרון אחר יתאים יותר לתרחיש השימוש שלכם.

העברת פרמטרים מפורשים

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

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