ההעברה מ-Views ל-Compose קשורה רק לממשק המשתמש, אבל יש הרבה דברים שצריך לקחת בחשבון כדי לבצע העברה בטוחה וצבירתית. בדף הזה מפורטים כמה שיקולים שצריך לקחת בחשבון כשעוברים מאפליקציה מבוססת-תצוגה ל-Compose.
העברת העיצוב של האפליקציה
מערכת העיצוב המומלצת ליצירת עיצוב מותאם אישית לאפליקציות ל-Android היא Material Design.
באפליקציות מבוססות-תצוגה יש שלוש גרסאות של Material:
- Material Design 1 באמצעות ספריית AppCompat (כלומר
Theme.AppCompat.*
) - Material Design 2 באמצעות ספריית MDC-Android (כלומר
Theme.MaterialComponents.*
) - Material Design 3 באמצעות ספריית MDC-Android (כלומר
Theme.Material3.*
)
באפליקציות Compose יש שתי גרסאות של Material:
- Material Design 2 באמצעות ספריית Compose Material (כלומר
androidx.compose.material.MaterialTheme
) - Material Design 3 באמצעות הספרייה Compose Material 3 (כלומר
androidx.compose.material3.MaterialTheme
)
מומלץ להשתמש בגרסה העדכנית ביותר (Material 3) אם מערכת העיצוב של האפליקציה מאפשרת זאת. יש מדריכים להעברה גם ל-Views וגם ל-Compose:
כשיוצרים מסכים חדשים ב-Compose, בלי קשר לגרסה של Material Design שבה אתם משתמשים, חשוב להחיל MaterialTheme
לפני כל רכיב Compose שמפיק ממשק משתמש מספריות Material של Compose. רכיבי Material (Button
, Text
וכו') תלויים בקיומו של MaterialTheme
, וההתנהגות שלהם לא מוגדרת בלעדיו.
בכל הדוגמאות ל-Jetpack Compose נעשה שימוש בעיצוב Compose מותאם אישית שנבנה על גבי MaterialTheme
.
מידע נוסף זמין במאמרים מערכות עיצוב ב-Compose והעברת נושאי XML ל-Compose.
ניווט
אם אתם משתמשים ברכיב הניווט באפליקציה, תוכלו לקרוא מידע נוסף במאמרים ניווט באמצעות Compose – יכולת פעולה הדדית והעברת ניווט ב-Jetpack ל-Navigation Compose.
בדיקה של ממשק המשתמש המשולב של Compose/Views
אחרי העברת חלקים מהאפליקציה ל-Compose, חשוב מאוד לבדוק שהכול תקין.
כשפעילות או קטע משתמשים ב-Compose, צריך להשתמש ב-createAndroidComposeRule
במקום ב-ActivityScenarioRule
. createAndroidComposeRule
משלב את ActivityScenarioRule
עם ComposeTestRule
שמאפשר לבדוק את הקוד בזמן הכתיבה והצגה.
class MyActivityTest { @Rule @JvmField val composeTestRule = createAndroidComposeRule<MyActivity>() @Test fun testGreeting() { val greeting = InstrumentationRegistry.getInstrumentation() .targetContext.resources.getString(R.string.greeting) composeTestRule.onNodeWithText(greeting).assertIsDisplayed() } }
מידע נוסף על בדיקה זמין במאמר בדיקת הפריסה של Compose. למידע על יכולת פעולה הדדית עם מסגרות לבדיקת ממשק משתמש, ראו יכולת פעולה הדדית עם Espresso ויכולת פעולה הדדית עם UiAutomator.
שילוב של Compose עם ארכיטקטורת האפליקציה הקיימת
דפוסי ארכיטקטורה של Unidirectional Data Flow (UDF) פועלים בצורה חלקה עם Compose. אם במקום זאת האפליקציה משתמשת בסוגים אחרים של דפוסי ארכיטקטורה, כמו Model View Presenter (MVP), מומלץ להעביר את החלק הזה של ממשק המשתמש ל-UDF לפני או במהלך ההטמעה של Compose.
שימוש ב-ViewModel
ב'כתיבה מהירה'
אם אתם משתמשים בספרייה Architecture ComponentsViewModel
, אתם יכולים לגשת ל-ViewModel
מכל רכיב שאפשר ליצור ממנו קומפוזיציה, על ידי קריאה לפונקציה viewModel()
, כפי שמוסבר במאמר Compose וספריות אחרות.
כשעוברים ל-Compose, חשוב להיזהר כשמשתמשים באותו סוג ViewModel
ברכיבים שונים של Compose, כי רכיבי ViewModel
פועלים בהתאם להיקפים של מחזור החיים של התצוגה. ההיקף יהיה הפעילות, החלק או תרשים הניווט של המארח, אם נעשה שימוש בספריית הניווט.
לדוגמה, אם הרכיבים הניתנים לשילוב מתארחים בפעילות, הפונקציה viewModel()
תמיד מחזירה את אותה מכונה, שמתבטלת רק בסיום הפעילות.
בדוגמה הבאה, אותו משתמש ('user1') מקבל הודעה פעמיים כי נעשה שימוש חוזר באותה מכונה של GreetingViewModel
בכל הרכיבים הניתנים לשילוב בפעילות המארח. המכונה הראשונה של ViewModel
שנוצרת משמשת שוב ברכיבים אחרים.
class GreetingActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MaterialTheme { Column { GreetingScreen("user1") GreetingScreen("user2") } } } } } @Composable fun GreetingScreen( userId: String, viewModel: GreetingViewModel = viewModel( factory = GreetingViewModelFactory(userId) ) ) { val messageUser by viewModel.message.observeAsState("") Text(messageUser) } class GreetingViewModel(private val userId: String) : ViewModel() { private val _message = MutableLiveData("Hi $userId") val message: LiveData<String> = _message } class GreetingViewModelFactory(private val userId: String) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { return GreetingViewModel(userId) as T } }
מאחר שגם תרשים הניווט מגדיר את ההיקף של רכיבי ViewModel
, לרכיבים הניתנים לקיבוץ שמשמשים כיעד בתרשים ניווט יש מופע אחר של ViewModel
.
במקרה כזה, ההיקף של ViewModel
הוא מחזור החיים של היעד, והוא נמחק כשהיעד מוסר מהמקבץ הקודם. בדוגמה הבאה, כשהמשתמש עובר למסך פרופיל, נוצרת מכונה חדשה של GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
מקור האמת של המצב
כשמשתמשים ב-Compose בחלק אחד של ממשק המשתמש, יכול להיות ש-Compose וקוד המערכת של View יצטרכו לשתף נתונים. כשהדבר אפשרי, מומלץ להכיל את המצב המשותף הזה בתוך סוג אחר שעומד בשיטות המומלצות ל-UDF ששתי הפלטפורמות משתמשות בהן. לדוגמה, ב-ViewModel
שחושף את המקור של הנתונים המשותפים כדי לשדר עדכוני נתונים.
עם זאת, לא תמיד אפשר לעשות זאת אם הנתונים שרוצים לשתף משתנים או מקושרים באופן הדוק לרכיב בממשק המשתמש. במקרה כזה, מערכת אחת צריכה להיות מקור האמת, והמערכת הזו צריכה לשתף את עדכוני הנתונים עם המערכת השנייה. ככלל אצבע, הבעלות על מקור המידע האמין צריכה להיות של הרכיב הקרוב יותר לשורש היררכיית ממשק המשתמש.
כתיבת תוכן כמקור האמת
משתמשים ב-composable SideEffect
כדי לפרסם את המצב של Compose בקוד שאינו של Compose. במקרה כזה, מקור האמת נשמר ב-composable ששולח עדכוני מצב.
לדוגמה, ייתכן שספריית הניתוח תאפשר לכם לפלח את אוכלוסיית המשתמשים על ידי צירוף מטא-נתונים מותאמים אישית (מאפייני משתמשים בדוגמה הזו) לכל האירועים הבאים בניתוח. כדי לעדכן את הערך של SideEffect
ולשלוח את סוג המשתמש של המשתמש הנוכחי לספריית Analytics, משתמשים ב-SideEffect
.
@Composable fun rememberFirebaseAnalytics(user: User): FirebaseAnalytics { val analytics: FirebaseAnalytics = remember { FirebaseAnalytics() } // On every successful composition, update FirebaseAnalytics with // the userType from the current User, ensuring that future analytics // events have this metadata attached SideEffect { analytics.setUserProperty("userType", user.userType) } return analytics }
מידע נוסף זמין במאמר תופעות לוואי ב-Compose.
המערכת כמקור המידע האמין
אם מערכת התצוגה היא הבעלים של המצב ומשתפת אותו עם Compose, מומלץ לעטוף את המצב באובייקטים מסוג mutableStateOf
כדי להפוך אותו למאובטח לשרשור עבור Compose. אם משתמשים בגישה הזו, הפונקציות הניתנות לקיפול פשוטות יותר כי אין להן יותר את מקור האמת, אבל מערכת התצוגה צריכה לעדכן את המצב המשתנה ואת התצוגות שמשתמשות במצב הזה.
בדוגמה הבאה, CustomViewGroup
מכיל TextView
ו-ComposeView
עם TextField
שאפשר ליצור ממנו רכיב. ב-TextView
צריך להופיע התוכן שהמשתמש מקלידים ב-TextField
.
class CustomViewGroup @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : LinearLayout(context, attrs, defStyle) { // Source of truth in the View system as mutableStateOf // to make it thread-safe for Compose private var text by mutableStateOf("") private val textView: TextView init { orientation = VERTICAL textView = TextView(context) val composeView = ComposeView(context).apply { setContent { MaterialTheme { TextField(value = text, onValueChange = { updateState(it) }) } } } addView(textView) addView(composeView) } // Update both the source of truth and the TextView private fun updateState(newValue: String) { text = newValue textView.text = newValue } }
העברת ממשק משתמש משותף
אם אתם עוברים בהדרגה ל-Compose, יכול להיות שתצטרכו להשתמש ברכיבי ממשק משתמש משותפים גם ב-Compose וגם במערכת התצוגה. לדוגמה, אם באפליקציה שלכם יש רכיב CallToActionButton
בהתאמה אישית, יכול להיות שתצטרכו להשתמש בו גם במסכים של Compose וגם במסכים מבוססי-תצוגה.
ב-Compose, רכיבי ממשק משתמש משותפים הופכים לרכיבים שאפשר לעשות בהם שימוש חוזר באפליקציה, ללא קשר לסגנון של הרכיב באמצעות XML או לכך שהוא תצוגה מותאמת אישית. לדוגמה, יוצרים רכיב CallToActionButton
שמורכב מרכיב Button
של קריאה מותאמת אישית לפעולה.
כדי להשתמש ב-composable במסכים שמבוססים על תצוגה, צריך ליצור מעטפת תצוגה בהתאמה אישית שמתרחבת מ-AbstractComposeView
. ברכיב הקומפוזיציה Content
שבו בוטל השינוי, צריך להציב את הרכיב הקומפוזיציה שיצרתם, עטוף בעיצוב של Compose, כפי שמתואר בדוגמה הבאה:
@Composable fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) { Button( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondary ), onClick = onClick, modifier = modifier, ) { Text(text) } } class CallToActionViewButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 ) : AbstractComposeView(context, attrs, defStyle) { var text by mutableStateOf("") var onClick by mutableStateOf({}) @Composable override fun Content() { YourAppTheme { CallToActionButton(text, onClick) } } }
שימו לב שהפרמטרים הניתנים ליצירה הופכים למשתנים שניתן לשנות בתוך התצוגה בהתאמה אישית. כך אפשר להרחיב את התצוגה המותאמת אישית CallToActionViewButton
ולהשתמש בה, כמו בתצוגה רגילה. דוגמה לכך עם קישור תצוגה מופיעה בהמשך:
class ViewBindingActivity : ComponentActivity() { private lateinit var binding: ActivityExampleBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityExampleBinding.inflate(layoutInflater) setContentView(binding.root) binding.callToAction.apply { text = getString(R.string.greeting) onClick = { /* Do something */ } } } }
אם הרכיב המותאם אישית מכיל מצב שאפשר לשנות, כדאי לעיין במאמר מקור האמת של המצב.
קביעת עדיפות לפיצול המצב מהמצגת
באופן מסורתי, View
הוא בעל מצב (stateful). View
מנהל שדות שמתארים מה להציג, בנוסף לאופן שבו להציג אותו. כשממירים View
ל-Compose, כדאי להפריד את הנתונים שעוברים עיבוד כדי ליצור זרימת נתונים חד-כיוונית, כפי שמוסבר בהרחבה בקטע העלאת המצב.
לדוגמה, ל-View
יש מאפיין visibility
שמתאר אם הוא גלוי, לא גלוי או לא קיים. זוהי תכונה מובנית של View
. קטעי קוד אחרים עשויים לשנות את החשיפה של View
, אבל רק View
עצמו יודע מהי החשיפה הנוכחית שלו. הלוגיקה של הבטחת החשיפה של View
עלולה להיות נוטה לשגיאות, ולרוב היא קשורה ל-View
עצמו.
לעומת זאת, ב-Compose קל להציג רכיבים מורכבים שונים לגמרי באמצעות לוגיקה מותנית ב-Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
מעצם הגדרתו, CautionIcon
לא צריך לדעת למה הוא מוצג או להתייחס לכך, ואין מושג של visibility
: הוא נמצא ב-Composition או שהוא לא נמצא בו.
כשמפרידים בבירור בין ניהול המצב לבין לוגיקה של הצגה, אפשר לשנות בצורה חופשית יותר את אופן הצגת התוכן כהמרה של המצב לממשק המשתמש. היכולת להעביר את המצב ל-hoist במקרה הצורך גם מאפשרת שימוש חוזר יעיל יותר ברכיבים הניתנים לשילוב, כי הבעלות על המצב היא גמישה יותר.
שימוש ברכיבים בקופסה (encapsulated) שניתנים לשימוש חוזר
לרכיבי View
יש לרוב מושג כלשהו לגבי המיקום שלהם: בתוך Activity
, Dialog
, Fragment
או במקום כלשהו בתוך היררכיית View
אחרת. מכיוון שהם לרוב מונפחים מקובצי פריסה סטטיים, המבנה הכללי של קובץ View
נוטה להיות נוקשה מאוד. כתוצאה מכך, יש קישור הדוק יותר, וקשה יותר לשנות או לעשות שימוש חוזר ב-View
.
לדוגמה, View
בהתאמה אישית עשוי להניח שיש לו תצוגת צאצא מסוג מסוים עם מזהה מסוים, ולשנות את המאפיינים שלו ישירות בתגובה לאיזו פעולה. כך מתבצעת קישור הדוק בין רכיבי ה-View
: ה-View
בהתאמה אישית עלול לקרוס או להיפגם אם הוא לא יכול למצוא את הצאצא, וכנראה שלא ניתן יהיה לעשות שימוש חוזר בצאצא בלי ההורה View
בהתאמה אישית.
הבעיה הזו פחות משמעותית ב-Compose עם רכיבים שניתנים לשימוש חוזר. ההורים יכולים לציין בקלות את המצב והקריאות החוזרות (callbacks), כך שתוכלו לכתוב רכיבים מורכבים לשימוש חוזר בלי לדעת את המיקום המדויק שבו הם ישמשו.
@Composable fun AScreen() { var isEnabled by rememberSaveable { mutableStateOf(false) } Column { ImageWithEnabledOverlay(isEnabled) ControlPanelWithToggle( isEnabled = isEnabled, onEnabledChanged = { isEnabled = it } ) } }
בדוגמה שלמעלה, כל שלושת החלקים מבודדים יותר ומקושרים פחות:
ImageWithEnabledOverlay
צריך לדעת רק מה המצב הנוכחי שלisEnabled
. הוא לא צריך לדעת ש-ControlPanelWithToggle
קיים, או איך אפשר לשלוט בו.ControlPanelWithToggle
לא יודע על קיומו שלImageWithEnabledOverlay
. יכול להיות שisEnabled
יוצג באפס, באחת או בכמה דרכים, ו-ControlPanelWithToggle
לא יצטרך להשתנות.לאב לא משנה כמה רמה עמוקה
ImageWithEnabledOverlay
אוControlPanelWithToggle
נמצאים בהיררכיה. הילדים האלה יכולים ליצור אנימציות של שינויים, להחליף תוכן או להעביר תוכן לילדים אחרים.
התבנית הזו נקראת הפיכת שליטה, וניתן לקרוא מידע נוסף עליה במסמכי העזרה של CompositionLocal
.
טיפול בשינויים בגודל המסך
שימוש במשאבים שונים לגדלים שונים של חלונות הוא אחת מהדרכים העיקריות ליצירת פריסות View
רספונסיביות. משאבים מותאמים עדיין יכולים לשמש לקבלת החלטות לגבי פריסה ברמת המסך, אבל ב-Compose קל הרבה יותר לשנות פריסות לגמרי בקוד באמצעות לוגיקה מותנית רגילה. מידע נוסף זמין במאמר שימוש בקטגוריות של גודל חלון.
בנוסף, כדאי לעיין במאמר תמיכה בגדלים שונים של מסכים כדי ללמוד על השיטות ש-Compose מציע ליצירת ממשקי משתמש מותאמים.
גלילה בתצוגות עץ באמצעות Views
במאמר יכולת פעולה הדדית של גלילה בתצוגה בתצוגה בתצוגה מוסבר איך מפעילים יכולת פעולה הדדית של גלילה בתצוגה בתצוגה בתצוגה בין רכיבי View שניתנים לגלילה לבין רכיבי composable שניתנים לגלילה, שמוטמעים בשני הכיוונים.
כתיבת הודעות ב-RecyclerView
ב-RecyclerView
, רכיבים מורכבים עובדים בצורה טובה מאז גרסת RecyclerView
1.3.0-alpha02. כדי ליהנות מהיתרונות האלה, צריך לוודא שאתם משתמשים בגרסה 1.3.0-alpha02 לפחות של RecyclerView
.
WindowInsets
יכולת פעולה הדדית עם Views
יכול להיות שתצטרכו לשנות את ברירת המחדל של הפריטים המוצגים בחלק הפנימי של המסך אם במסך שלכם מופיעים גם תצוגות וגם קוד Compose באותה היררכיה. במקרה כזה, צריך לציין בבירור איזה רכיב צריך לצרוך את ה-insets ואילו רכיבים צריכים להתעלם מהם.
לדוגמה, אם הפריסה החיצונית ביותר היא פריסה של Android View, צריך להשתמש ב-insets במערכת View ולהתעלם מהם ב-Compose.
לחלופין, אם הפריסה החיצונית ביותר היא רכיב Compose, צריך להשתמש ברכיבי ה-inset ב-Compose ולספק את הרווח הנדרש לרכיבי ה-Compose של AndroidView
בהתאם.
כברירת מחדל, כל ComposeView
צורך את כל ה-insets ברמת הצריכה WindowInsetsCompat
. כדי לשנות את התנהגות ברירת המחדל הזו, מגדירים את הערך של ComposeView.consumeWindowInsets
לערך false
.
מידע נוסף זמין במסמכי התיעוד בנושא WindowInsets
ב-Compose.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- הצגת אמוג'י
- עיצוב Material 2 ב-Compose
- הוספת חלונות משנה ב-Compose