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

CompositionLocal הוא כלי לכוונון עדין שמעבירים נתונים דרך היצירה באופן מרומז. הנושאים בדף הזה כאן אפשר ללמוד בפירוט על 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 – צבעים, טיפוגרפיה והצורות - כך שאפשר לאחזר אותן מאוחר יותר בכל חלק צאצא של יצירה מוזיקלית. באופן ספציפי, אלו הם LocalColors, LocalShapes LocalTypography נכסים שניתן לגשת אליהם דרך MaterialTheme colors, shapes ו-typography.

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, 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.colors.primary
    )
}

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

כדי לציין ערך חדש ל-CompositionLocal, צריך להשתמש בפונקציה CompositionLocalProvider ו-provides הפונקציה הקבועה שמשייכת מפתח CompositionLocal אל value. lambda אחד (content) מתוך CompositionLocalProvider יקבל כשניגשים למאפיין current של CompositionLocal. כאשר סופק ערך חדש, התכונה 'פיתוח נייטיב' מורכבת מחדש מחלקים ביצירה שקוראים CompositionLocal.

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

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha now")
}

איור 1. תצוגה מקדימה של התוכן הקומפוזבילי 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 מתקיים.

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

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

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

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

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

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

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

יצירת CompositionLocal

צריך ליצור שני ממשקי API כדי ליצור CompositionLocal:

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

  • staticCompositionLocalOf: שלא כמו compositionLocalOf, הקריאות של staticCompositionLocalOf אינן במעקב באמצעות 'כתיבה'. שינוי הערך גורם להצגה המלאה של 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 הפונקציה infix שמשייכת מפתח 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
    Card(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 בתוכן lambdas באותו אופן כדי לקבל אותם יתרונות:

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

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