ההעברה מ-Views ל-Compose קשורה רק לממשק המשתמש, אבל יש הרבה דברים שצריך לקחת בחשבון כדי לבצע העברה בטוחה והדרגתית. בדף הזה מפורטים כמה שיקולים שכדאי לקחת בחשבון כשמעבירים אפליקציה מבוססת-View ל-Compose.
העברת העיצוב של האפליקציה
Material Design היא מערכת העיצוב המומלצת לעיצוב של אפליקציות ל-Android.
באפליקציות מבוססות-תצוגה, יש שלוש גרסאות של 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. רכיבי Material (Button
, Text
וכו') תלויים ב-MaterialTheme
שמוגדר במקום, וההתנהגות שלהם לא מוגדרת בלעדיו.
כל הדוגמאות של Jetpack Compose מבוססות על עיצוב מותאם אישית של Compose שנבנה על בסיס MaterialTheme
.
מידע נוסף זמין במאמרים בנושא מערכות עיצוב ב-Compose והעברת ערכות נושא בפורמט XML ל-Compose.
ניווט
אם אתם משתמשים ברכיב הניווט באפליקציה, תוכלו לקרוא מידע נוסף במאמרים ניווט באמצעות Compose – יכולת פעולה הדדית והעברת Jetpack Navigation ל-Navigation Compose.
בדיקת ממשק משתמש משולב של כתיבה ותצוגות
אחרי העברת חלקים מהאפליקציה ל-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() } }
במאמר בדיקת הפריסה של כלי הכתיבה מוסבר איך לבצע בדיקות. למידע על פעולה הדדית עם מסגרות לבדיקת ממשק משתמש, אפשר לעיין במאמרים בנושא פעולה הדדית עם Espresso ופעולה הדדית עם UiAutomator.
שילוב של Compose עם ארכיטקטורת האפליקציה הקיימת
דפוסי ארכיטקטורה של זרימת נתונים חד-כיוונית (UDF) פועלים בצורה חלקה עם Compose. אם האפליקציה משתמשת בסוגים אחרים של תבניות ארכיטקטורה, כמו Model View Presenter (MVP), מומלץ להעביר את החלק הזה בממשק המשתמש ל-UDF לפני או במהלך ההטמעה של Compose.
שימוש ב-ViewModel
בכתיבת אימייל
אם אתם משתמשים בספרייה Architecture Components
ViewModel
, אתם יכולים לגשת אל ViewModel
מכל קומפוננטה שניתנת להרכבה על ידי קריאה לפונקציה viewModel()
, כמו שמוסבר במאמר Compose וספריות אחרות.
כשמשתמשים ב-Compose, צריך להיזהר משימוש באותו סוג ViewModel
בקומפוזיציות שונות, כי רכיבי ViewModel
פועלים בהיקפים של מחזור החיים של התצוגה. ההיקף יהיה פעילות המארח, קטע או תרשים הניווט אם נעשה שימוש בספריית הניווט.
לדוגמה, אם רכיבי ה-Composable מתארחים בפעילות, הפונקציה 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
מוגדר בהתאם למחזור החיים של היעד, והוא מתנקה כשהיעד מוסר מהערימה. בדוגמה הבאה, כשמשתמש עובר למסך Profile, נוצר מופע חדש של GreetingViewModel
.
@Composable fun MyApp() { NavHost(rememberNavController(), startDestination = "profile/{userId}") { /* ... */ composable("profile/{userId}") { backStackEntry -> GreetingScreen(backStackEntry.arguments?.getString("userId") ?: "") } } }
מקור האמת של המצב
כשמשתמשים ב-Compose בחלק אחד של ממשק המשתמש, יכול להיות ש-Compose וקוד המערכת של View יצטרכו לשתף נתונים. במקרים שבהם הדבר אפשרי, מומלץ להשתמש במצב המשותף הזה בתוך מחלקה אחרת שפועלת לפי השיטות המומלצות של UDF שמשמשות את שתי הפלטפורמות. לדוגמה, במחלקה ViewModel
שחושפת זרם של הנתונים המשותפים כדי לשלוח עדכוני נתונים.
עם זאת, זה לא תמיד אפשרי אם הנתונים שרוצים לשתף ניתנים לשינוי או קשורים באופן הדוק לרכיב בממשק המשתמש. במקרה כזה, מערכת אחת צריכה להיות מקור האמת, והמערכת הזו צריכה לשתף את כל עדכוני הנתונים עם המערכת השנייה. ככלל, המקור המהימן צריך להיות בבעלות הרכיב שקרוב יותר לשורש של היררכיית ממשק המשתמש.
Compose כמקור מידע אמין
אפשר להשתמש ב-composable
SideEffect
כדי לפרסם מצב Compose בקוד שאינו Compose. במקרה כזה, מקור האמת נשמר ברכיב שאפשר להרכיב, ושולח עדכוני מצב.
לדוגמה, יכול להיות שספריית ניתוח הנתונים תאפשר לכם לפלח את אוכלוסיית המשתמשים על ידי צירוף מטא-נתונים מותאמים אישית (מאפייני משתמש בדוגמה הזו) לכל אירועי ניתוח הנתונים הבאים. כדי להעביר את סוג המשתמש הנוכחי לספריית הניתוח, צריך להשתמש ב-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.
הצגת המערכת כמקור מידע אמין
אם המערכת View היא הבעלים של המצב והיא משתפת אותו עם 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 וגם במערכת View. לדוגמה, אם לאפליקציה יש רכיב מותאם אישית CallToActionButton
, יכול להיות שתצטרכו להשתמש בו גם במסכים מבוססי-Compose וגם במסכים מבוססי-View.
ב-Compose, רכיבי ממשק משתמש משותפים הופכים לפונקציות שאפשר להשתמש בהן שוב באפליקציה, בלי קשר לכך שהרכיב עוצב באמצעות XML או שהוא תצוגה בהתאמה אישית. לדוגמה, יוצרים CallToActionButton
רכיב שאפשר להרכיב ממנו רכיבים אחרים עבור רכיב הקריאה לפעולה בהתאמה אישיתButton
.
כדי להשתמש ב-composable במסכים מבוססי-תצוגה, צריך ליצור wrapper של תצוגה בהתאמה אישית שמתרחב מ-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
המותאמת אישית ולהשתמש בה, כמו בתצוגה רגילה. דוגמה לשימוש ב-View Binding:
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
הוא בעל מצב. View
מנהל שדות שמתארים מה להציג, בנוסף לאיך להציג את זה. כשממירים View
ל-Compose, כדאי להפריד את הנתונים שמוצגים כדי להשיג זרימת נתונים חד-כיוונית, כפי שמוסבר בהמשך במאמר בנושא state hoisting.
לדוגמה, לאובייקט View
יש מאפיין visibility
שמתאר אם הוא גלוי, לא גלוי או לא קיים. זוהי תכונה מובנית של View
. יכול להיות שחלקים אחרים בקוד ישנו את החשיפה של View
, אבל רק View
עצמו יודע מה החשיפה הנוכחית שלו. הלוגיקה שקובעת אם View
גלוי עלולה להיות בעייתית, ולרוב היא קשורה ל-View
עצמו.
לעומת זאת, ב-Compose קל להציג רכיבים שונים לחלוטין באמצעות לוגיקה מותנית ב-Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
מבחינת העיצוב, CautionIcon
לא צריך לדעת או להתעניין למה הוא מוצג, ואין מושג של visibility
: הוא נמצא ב-Composition או שהוא לא נמצא בה.
הפרדה ברורה בין ניהול המצב לבין לוגיקת ההצגה מאפשרת לכם לשנות את אופן הצגת התוכן בחופשיות רבה יותר, כהמרה של המצב לממשק משתמש. האפשרות להעביר את הסטייט כשצריך גם הופכת את הקומפוזבלים לשימושיים יותר, כי הבעלות על הסטייט גמישה יותר.
קידום רכיבים מוכללים וניתנים לשימוש חוזר
לרכיבי View
יש בדרך כלל מושג לגבי המיקום שלהם: בתוך Activity
, Dialog
, Fragment
או בתוך היררכיה אחרת של View
. מכיוון שהם מנופחים לעיתים קרובות מקובצי פריסה סטטיים, המבנה הכללי של View
נוטה להיות נוקשה מאוד. התוצאה היא צימוד הדוק יותר, וקשה יותר לשנות או לעשות שימוש חוזר בView
.
לדוגמה, יכול להיות שרכיב View
מותאם אישית מניח שיש לו תצוגת צאצא מסוג מסוים עם מזהה מסוים, ומשנה את המאפיינים שלו ישירות בתגובה לפעולה מסוימת. הפעולה הזו יוצרת קשר הדוק בין רכיבי View
האלה: יכול להיות שרכיב האב המותאם אישית View
יקרוס או ייפגם אם הוא לא ימצא את רכיב הבן, וסביר להניח שאי אפשר יהיה לעשות שימוש חוזר ברכיב הבן בלי רכיב האב המותאם אישית View
.
זו פחות בעיה ב-Compose עם קומפוזיציות שאפשר לעשות בהן שימוש חוזר. ההורים יכולים לציין בקלות מצב וקריאות חוזרות (callbacks), כך שאפשר לכתוב פונקציות composable לשימוש חוזר בלי לדעת בדיוק איפה הן ישמשו.
@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 שניתן לגלול אותם לבין רכיבי Composables שניתן לגלול אותם, מקוננים בשני הכיוונים, זמין במאמר יכולת פעולה הדדית של גלילה מקוננת.
כתיבה ב-RecyclerView
רכיבי Composables ב-RecyclerView
פועלים בצורה יעילה מגרסה RecyclerView
1.3.0-alpha02. כדי לראות את ההטבות האלה, צריך לוודא שאתם משתמשים לפחות בגרסה 1.3.0-alpha02 של RecyclerView
.
WindowInsets
יכולת פעולה הדדית עם Views
יכול להיות שתצטרכו לבטל את ברירת המחדל של השוליים הפנימיים אם במסך שלכם יש גם רכיבי View וגם קוד Compose באותה היררכיה. במקרה כזה, צריך לציין במפורש לאיזה מהם להשתמש בתוספות ולאיזה מהם להתעלם מהן.
לדוגמה, אם הפריסה החיצונית ביותר היא פריסת View ב-Android, צריך להשתמש ב-insets במערכת View ולהתעלם מהם ב-Compose.
לחלופין, אם הפריסה החיצונית ביותר היא קומפוזיציה, צריך להשתמש ב-insets ב-Compose, ולשנות את ה-padding של הקומפוזיציות בהתאם.AndroidView
כברירת מחדל, כל ComposeView
צורך את כל ההנחות ברמת הצריכה WindowInsetsCompat
. כדי לשנות את התנהגות ברירת המחדל הזו, צריך להגדיר את
ComposeView.consumeWindowInsets
לערך false
.
מידע נוסף זמין במאמר בנושא WindowInsets
ב-Compose.
מומלץ עבורך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- הצגת אמוג'י
- Material Design 2 ב-Compose
- שוליים פנימיים של חלונות בכתיבת אימייל