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 ) }
כדי שלא יהיה צורך להעביר את הצבעים כתלויות מפורשת של פרמטר לרוב התכנים הקומפוזביליים, אפשר להשתמש בתכונה כתיבת הצעות CompositionLocal
שמאפשרת ליצור אובייקטים עם שם ברמת העץ ולהשתמש בהם כדרך מרומזת להעברת נתונים דרך העץ של ממשק המשתמש.
בדרך כלל, רכיבי CompositionLocal
מקבלים ערך בצומת מסוים של עץ ממשק המשתמש. הצאצאים הניתנים ליצירה של הפונקציה יכולים להשתמש בערך הזה בלי להצהיר על CompositionLocal
כפרמטר בפונקציה הניתנת ליצירה.
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
שלה, שמשייכות מפתח CompositionLocal
ל-value
. פונקציית ה-lambda content
של CompositionLocalProvider
תקבל את הערך שסופק כשניגשים לנכס current
של CompositionLocal
. כשמציינים ערך חדש, Compose יוצר מחדש חלקים מהקומפוזיציה שקוראים את 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
היו בשימוש פנימי על ידי תכנים קומפוזביליים של 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
מקשה על ההבנה של ההתנהגות של רכיב ה-Composable. בגלל שהם יוצרים יחסי תלות מרומזים, המתקשרים של תכנים קומפוזביליים שמשתמשים בהם צריכים לוודא שהערך של כל CompositionLocal
מתקיים.
בנוסף, יכול להיות שלא יהיה מקור ברור לאמת של התלות הזו, כי היא יכולה להשתנות בכל חלק של ה-Composition. לכן, ניפוי באגים באפליקציה כשמתרחשת בעיה יכול להיות מאתגר יותר, כי צריך לנווט בהרכב כדי לראות איפה צוין הערך current
. כלים כמו חיפוש שימושים בסביבת הפיתוח המשולבת (IDE) או הכלי לבדיקת פריסת הכתיבה מספקים מספיק מידע כדי לפתור את הבעיה הזו.
ההחלטה אם להשתמש ב-CompositionLocal
יש תנאים מסוימים שבהם CompositionLocal
יכול להיות פתרון טוב לתרחיש לדוגמה שלכם:
ל-CompositionLocal
צריך להיות ערך ברירת מחדל טוב. אם אין ערך ברירת מחדל, עליכם להבטיח שמפתח יתקשה מאוד להגיע למצב שבו לא יסופק ערך ל-CompositionLocal
.
אם לא תספקו ערך ברירת מחדל, יכול להיות שתתקלו בבעיות ובתסכול כשתיצרו בדיקות או תציגו תצוגה מקדימה של רכיב מורכב שמשתמש ב-CompositionLocal
, כי תמיד תצטרכו לספק אותו באופן מפורש.
אין להשתמש ב-CompositionLocal
לקונספטים שלא נחשבים ברמת העץ או ברמת היררכיית המשנה. כדאי להשתמש ב-CompositionLocal
כשכל הצאצאים יכולים להשתמש בו, ולא רק חלק מהם.
אם תרחיש השימוש שלכם לא עומד בדרישות האלה, כדאי לעיין בקטע חלופות שכדאי לשקול לפני יצירת CompositionLocal
.
דוגמה לשימוש לא טוב היא יצירת CompositionLocal
שמכיל את ViewModel
של מסך מסוים, כדי שכל הרכיבים הניתנים ליצירה במסך הזה יוכלו לקבל הפניה ל-ViewModel
כדי לבצע לוגיקה כלשהי. זוהי שיטה לא טובה, כי לא כל הרכיבים הניתנים ליצירה מתחת לעץ מסוים של ממשק משתמש צריכים לדעת על ViewModel
. מומלץ להעביר לרכיבים הניתנים לשילוב רק את המידע הנדרש להם, לפי התבנית שבה המצב זורם למטה והאירועים זורמים למעלה. בצורה הזו, קל יותר לבדוק את התכנים הקומפוזביליים ולהשתמש בהם.
יצירת CompositionLocal
יש שני ממשקי API ליצירת CompositionLocal
:
compositionLocalOf
: שינוי הערך שסופק במהלך הרכבת מחדש מבטל רק את התוכן שקורא את הערך שלו ב-current
.staticCompositionLocalOf
: בניגוד ל-compositionLocalOf
, Compose לא עוקב אחרי קריאות שלstaticCompositionLocalOf
. שינוי הערך גורם להרכבת מחדש של כל ה-lambda שלcontent
במקום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, סביר להניח שפתרון אחר יתאים יותר לתרחיש השימוש שלכם.
העברת פרמטרים מפורשים
גילוי מפורש לגבי יחסי תלות של תוכן קומפוזבילי הוא הרגל טוב. מומלץ להעביר לרכיבים הניתנים לקישור רק את מה שהם צריכים. כדי לעודד פענוח של תכנים קומפוזביליים ושימוש חוזר בהם, כל תוכן קומפוזבילי צריך להכיל את כמות המידע המינימלית האפשרית.
@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 גמישים יותר ברמה נמוכה יותר.
באופן דומה, ניתן להשתמש ב-lambdas של @Composable
באותו אופן כדי ליהנות מאותם יתרונות:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- מבנה של נושא ב-Compose
- שימוש בתצוגות ב'כתיבה'
- Kotlin ל-Jetpack פיתוח נייטיב