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
כפרמטר בפונקציה הניתנת ליצירה.
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
. הפונקציה הלוגרית 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. לכן, ניפוי באגים באפליקציה כשמתרחשת בעיה יכול להיות מאתגר יותר, כי צריך לנווט למעלה ב-Composition כדי לראות איפה הוענק הערך current
. כלים כמו Find
usages בסביבת הפיתוח המשולבת או Compose layout inspector מספקים מספיק מידע כדי למזער את הבעיה הזו.
החלטה אם להשתמש ב-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 גמישים יותר ברמה נמוכה יותר.
באופן דומה, אפשר להשתמש ב-lambda של תוכן @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 פיתוח נייטיב