המעבר מ-Views ל-Compose קשור רק לממשק המשתמש, אבל יש הרבה דברים שצריך להביא בחשבון כדי לבצע העברה בטוחה והדרגתית. בדף הזה מפורטים כמה שיקולים שכדאי להביא בחשבון כשמעבירים אפליקציה מבוססת-View לפיתוח נייטיב.
העברת העיצוב של האפליקציה
Material Design היא מערכת העיצוב המומלצת ליצירת עיצובים לאפליקציות ל-Android.
לאפליקציות שמבוססות על View, יש שלוש גרסאות של 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:
- מעבר מ-Material 1 ל-Material 2 ב-Views
- מעבר מ-Material 2 ל-Material 3 ב-Views
- מעבר מ-Material 2 ל-Material 3 ב-Compose
כשיוצרים מסכים חדשים ב-Compose, לא משנה באיזו גרסה של Material Design משתמשים, חשוב להחיל MaterialTheme לפני כל רכיב Composable שיוצר ממשק משתמש מספריות Compose Material. רכיבי Material (Button, Text וכו') תלויים בקיום של MaterialTheme, וההתנהגות שלהם לא מוגדרת בלעדיו.
כל הדוגמאות של Jetpack Compose משתמשות בעיצוב מותאם אישית של Compose שמבוסס על MaterialTheme.
מידע נוסף זמין במאמרים מערכות עיצוב ב-Compose והעברת עיצובים בפורמט XML ל-Compose.
ניווט
אם אתם משתמשים ברכיב הניווט באפליקציה, מידע נוסף זמין במאמרים ניווט מפיתוח נייטיב באפליקציה מבוססת-מקטע (fragment) והעברת נתונים מ-Jetpack Navigation ל-Navigation Compose.
בדיקת ממשק משתמש משולב של פיתוח נייטיב ו-Views
אחרי שמעבירים חלקים מהאפליקציה ל-Compose, חשוב מאוד לבדוק אותה כדי לוודא ששום דבר לא השתבש.
כשפעילות או מקטע משתמשים ב-Compose, צריך להשתמש ב-createAndroidComposeRule במקום ב-ActivityScenarioRule. createAndroidComposeRule משתלב עם ActivityScenarioRule באמצעות ComposeTestRule שמאפשר לבדוק את הקוד של Compose ושל View בו-זמנית.
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.
שילוב של פיתוח נייטיב בארכיטקטורת האפליקציה הקיימת
תבניות ארכיטקטורה של זרימת נתונים חד-כיוונית (UDF) פועלות בצורה חלקה עם Compose. אם האפליקציה משתמשת בסוגים אחרים של דפוסי ארכיטקטורה, כמו Model View Presenter (MVP), מומלץ להעביר את החלק הזה של ממשק המשתמש ל-UDF לפני או במהלך ההטמעה של Compose.
שימוש ב-ViewModel בפיתוח נייטיב
אם אתם משתמשים בספרייה Architecture Components
ViewModel, אתם יכולים לגשת ל-ViewModel מכל פונקציה קומפוזבילית באמצעות קריאה לפונקציה viewModel(), כמו שמוסבר במאמר Compose וספריות אחרות.
כשמשתמשים ב-Compose, חשוב להיזהר כשמשתמשים באותו סוג של ViewModel ברכיבים קומפוזביליים שונים, כי רכיבי 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 מוגדר למחזור החיים של היעד, והוא נמחק כשהיעד מוסר מהמחסנית האחורית. בדוגמה הבאה, כשהמשתמש מנווט למסך 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 כמקור המידע האמין
משתמשים ברכיב הקומפוזבילי SideEffect כדי לפרסם את המצב של פיתוח נייטיב בקוד שלא קשור לפיתוח נייטיב. במקרה הזה, מקור האמת נשמר ב-composable, ששולח עדכוני מצב.
לדוגמה, ספריית הניתוח מאפשרת לפלח את אוכלוסיית המשתמשים על ידי צירוף מטא-נתונים מותאמים אישית (מאפייני משתמש בדוגמה הזו) לכל אירועי הניתוח הבאים. כדי להעביר את סוג המשתמש הנוכחי לספריית הניתוח, צריך להשתמש ב-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 }
מידע נוסף זמין במאמר תופעות לוואי בפיתוח נייטיב.
הצגת המערכת כמקור המידע המהימן
אם מערכת View היא הבעלים של המצב ומשתפת אותו עם Compose, מומלץ לעטוף את המצב באובייקטים מסוג mutableStateOf כדי להפוך אותו ל-thread-safe בשביל Compose. אם משתמשים בגישה הזו, הפונקציות הקומפוזביליות פשוטות יותר כי הן כבר לא מכילות את מקור האמת, אבל מערכת ה-View צריכה לעדכן את המצב הניתן לשינוי ואת ה-Views שמשתמשים במצב הזה.
בדוגמה הבאה, 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 } }
העברת ממשק משתמש משותף
אם אתם עוברים בהדרגה לפיתוח נייטיב, יכול להיות שתצטרכו להשתמש ברכיבים משותפים של ממשק המשתמש גם בפיתוח נייטיב וגם במערכת View. לדוגמה, אם באפליקציה יש רכיב CallToActionButton בהתאמה אישית, יכול להיות שתצטרכו להשתמש בו גם במסכים שמבוססים על פיתוח נייטיב וגם במסכים שמבוססים על View.
ב-Compose, רכיבים משותפים בממשק המשתמש הופכים לרכיבים שאפשר להשתמש בהם שוב באפליקציה, גם אם הרכיב מעוצב באמצעות XML או שהוא תצוגה בהתאמה אישית. לדוגמה, אתם יכולים ליצור פונקציית קומפוזציה מסוג CallToActionButton לרכיב Button של הקריאה לפעולה בהתאמה אישית.
כדי להשתמש בפונקציה הקומפוזבילית במסכים שמבוססים על תצוגות, יוצרים 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) } } }
שימו לב שהפרמטרים של ה-composable הופכים למשתנים ניתנים לשינוי בתוך התצוגה המותאמת אישית. כך אפשר להרחיב את התצוגה המותאמת אישית של 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 הוא בעל מצב (stateful). View מנהל שדות שמתארים מה להציג, בנוסף לאיך להציג את זה. כשממירים View ל-Compose, כדאי להפריד את הנתונים שעוברים רינדור כדי להשיג זרימת נתונים חד-כיוונית, כפי שמוסבר בהרחבה בקטע העלאת הרמה של מצב (state hoisting).
לדוגמה, ל-View יש מאפיין visibility שמתאר אם הוא גלוי, לא גלוי או נעלם. זהו מאפיין מובנה של View. יכול להיות שקטעי קוד אחרים ישנו את החשיפה של View, אבל רק View עצמו יודע מה החשיפה הנוכחית שלו. הלוגיקה שמוודאת ש-View גלוי יכולה להיות מועדת לשגיאות, ולעיתים קרובות היא קשורה ל-View עצמו.
לעומת זאת, ב-Compose קל להציג רכיבים שונים לחלוטין באמצעות לוגיקה מותנית ב-Kotlin:
@Composable fun MyComposable(showCautionIcon: Boolean) { if (showCautionIcon) { CautionIcon(/* ... */) } }
כך זה תוכנן: CautionIcon לא צריך לדעת או להתייחס לסיבה שהוא מוצג, ואין מושג של visibility: הוא נמצא בקומפוזיציה או שהוא לא נמצא בה.
הפרדה ברורה בין ניהול המצב לבין לוגיקת התצוגה מאפשרת לכם לשנות באופן חופשי יותר את אופן הצגת התוכן כהמרה של מצב לממשק משתמש. היכולת להניף מצב כשצריך גם הופכת את הרכיבים הניתנים לשילוב ליותר ניתנים לשימוש חוזר, כי הבעלות על המצב גמישה יותר.
קידום רכיבים מובנים ורכיבים שניתן לעשות בהם שימוש חוזר
לרכיבי 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 מציע ליצירת ממשקי משתמש מותאמים.
גלילה בתצוגות מקוננות
במאמר יכולת פעולה הדדית של גלילה בתצוגות עץ מוסבר איך להפעיל יכולת פעולה הדדית של גלילה בתצוגות עץ בין רכיבי View שניתן לגלול בהם לבין רכיבים קומפוזביליים שניתן לגלול בהם, בתצוגת עץ בשני הכיוונים.
כתיבה ב-RecyclerView
רכיבי Composable ב-RecyclerView הם בעלי ביצועים גבוהים החל מגרסה 1.3.0-alpha02 של RecyclerView. כדי ליהנות מהיתרונות האלה, ודאו שאתם משתמשים בגרסה 1.3.0-alpha02 ומעלה של RecyclerView.
יכולת פעולה הדדית של WindowInsets עם Views
יכול להיות שתצטרכו לשנות את ברירת המחדל של השוליים הפנימיים כשהמסך מכיל גם קוד של תצוגות וגם קוד של Compose באותה היררכיה. במקרה כזה, צריך לציין במפורש איזה רכיב צריך להשתמש ב-inset ואיזה רכיב צריך להתעלם מהם.
לדוגמה, אם הפריסה החיצונית ביותר היא פריסת Android View, צריך לצרוך את ה-insets במערכת View ולהתעלם מהם ב-Compose.
לחלופין, אם הפריסה החיצונית ביותר היא פונקציה קומפוזבילית, צריך לצרוך את המרווחים ב-Compose ולהוסיף פדינג לפונקציות הקומפוזביליות של AndroidView בהתאם.
כברירת מחדל, כל ComposeView צורך את כל ה-insets ברמת הצריכה WindowInsetsCompat. כדי לשנות את פעולת ברירת המחדל, מגדירים את ComposeView.consumeWindowInsets לערך false.
מידע נוסף זמין במסמכי התיעוד בנושא WindowInsets ב-Compose.
מומלץ בשבילך
- הערה: טקסט הקישור מוצג כש-JavaScript מושבת
- הצגת אמוג'י
- Material Design 2 ב-Compose
- הוספת חלונות ב-Compose