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
תואם לערך הכי קרוב שסופק על ידי רכיב אב בחלק הזה של ה-Composition.
כדי לספק ערך חדש ל-CompositionLocal
, משתמשים בפונקציה CompositionLocalProvider
ובפונקציה provides
שמופיעה באמצע הביטוי, שמשייכת מפתח 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") }
איור 1. תצוגה מקדימה של הרכיב CompositionLocalExample
.
בדוגמה האחרונה, מופעי CompositionLocal
היו בשימוש פנימי ברכיבי Material composable. כדי לגשת לערך הנוכחי של 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
הוא כלי להעברת נתונים דרך רכיב ה-Composition באופן מרומז.
אות חשוב נוסף לשימוש ב-CompositionLocal
הוא כשמדובר בפרמטר חוצה-חתכים ושכבות ביניים של הטמעה לא צריכות להיות מודעות לקיומו, כי אם השכבות האלה יהיו מודעות לקיומו, זה יגביל את השימושיות של הקומפוזיציה. לדוגמה, שאילתה לגבי הרשאות ב-Android מתבצעת באמצעות CompositionLocal
. רכיב מדיה לבחירה (composable) יכול להוסיף פונקציונליות חדשה לגישה לתוכן שמוגן על ידי הרשאות במכשיר, בלי לשנות את ה-API שלו ולדרוש מהמתקשרים של רכיב המדיה לבחירה להיות מודעים להקשר הנוסף הזה שמשמש את הסביבה.
עם זאת, CompositionLocal
לא תמיד הפתרון הכי טוב. אנחנו ממליצים לא להשתמש יותר מדי ב-CompositionLocal
כי יש לכך כמה חסרונות:
CompositionLocal
מקשה על הבנת ההתנהגות של פונקציה שאפשר להרכיב. הם יוצרים תלויות מרומזות, ולכן מי שקורא לפונקציות קומפוזיציה שמשתמשות בהם צריך לוודא שמוגדר ערך לכל 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 (הסטטוס זורם למטה והאירועים זורמים למעלה). הגישה הזו תאפשר לכם לעשות שימוש חוזר ברכיבי ה-Composable שלכם בקלות רבה יותר, וגם לבדוק אותם בקלות רבה יותר.
יצירת CompositionLocal
יש שני ממשקי API ליצירת CompositionLocal
:
compositionLocalOf
: שינוי הערך שסופק במהלך ההרכבה מחדש מבטל את התוקף של התוכן בלבד שקורא את הערך שלcurrent
.
staticCompositionLocalOf
: בניגוד ל-compositionLocalOf
, קריאות שלstaticCompositionLocalOf
לא נרשמות על ידי Compose. שינוי הערך גורם להרכבה מחדש של כל פונקציית ה-content
lambda שבה מסופק ה-CompositionLocal
, במקום רק המקומות שבהם הערך שלcurrent
נקרא בהרכבה.
אם הערך שצוין במאפיין CompositionLocal
לא צפוי להשתנות או שלא ישתנה לעולם, כדאי להשתמש במאפיין staticCompositionLocalOf
כדי לשפר את הביצועים.
לדוגמה, מערכת העיצוב של אפליקציה יכולה להיות דעתנית לגבי האופן שבו פונקציות Composable מועלות באמצעות צל עבור רכיב ממשק המשתמש. מכיוון שהגבהים השונים של האפליקציה צריכים להתפשט בכל עץ ממשק המשתמש, אנחנו משתמשים ב-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
, משתמשים בפונקציית ה-infix 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
content lambdas באותו אופן כדי ליהנות מאותם יתרונות:
@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 פיתוח נייטיב