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