ההעברה מ-Views ל-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:
כשיוצרים מסכים חדשים ב-Composer, בלי קשר לגרסה של Material Design, חשוב לוודא שמגדירים MaterialTheme
לפני תכנים קומפוזביליים שפולטים ממשק משתמש מהספריות של Material Design. רכיבי Material (Button
, Text
וכו') תלויים בקיומו של MaterialTheme
, וההתנהגות שלהם לא מוגדרת בלעדיו.
בכל הדוגמאות ל-Jetpack Compose נעשה שימוש בעיצוב Compose מותאם אישית שנבנה על גבי MaterialTheme
.
למידע נוסף, ראו עיצוב מערכות בכתיבה והעברת עיצובי XML לכתיבה.
ניווט
אם אתם משתמשים ברכיב הניווט באפליקציה, תוכלו לקרוא מידע נוסף בקטע ניווט באמצעות 'כתיבה' – יכולת פעולה הדדית והעברת ניווט ב-Jetpack לכתיבה קולית.
בדיקת ממשק המשתמש המשולב של 'כתיבה'/'תצוגות'
אחרי העברת חלקים מהאפליקציה ל-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.
שילוב של פיתוח נייטיב עם ארכיטקטורת האפליקציה הקיימת
דפוסי ארכיטקטורה של 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
הוא מחזור החיים של היעד, והוא נמחק כשהיעד מוסר מה-backstack. בדוגמה הבאה, כשהמשתמש עובר למסך פרופיל, נוצרת מכונה חדשה של 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
כדי לעדכן את הערך שלו.
@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 } }
העברת ממשק משתמש משותף
אם אתם עוברים בהדרגה לכתיבה, יכול להיות שתצטרכו להשתמש ברכיבים משותפים של ממשק המשתמש גם במערכת הכתיבה וגם במערכת התצוגה המפורטת. לדוגמה, אם האפליקציה שלכם כוללת רכיב CallToActionButton
מותאם אישית, ייתכן שתצטרכו להשתמש בו גם במסך 'כתיבה' וגם במסך שמבוסס על תצוגה.
בפיתוח של רכיבים משותפים בממשק המשתמש, הם הופכים לתכנים קומפוזביליים שאפשר לעשות בהם שימוש חוזר בכל האפליקציה, בלי קשר לסגנון של הרכיב באמצעות 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) } } }
שימו לב שהפרמטרים הניתנים ליצירה הופכים למשתנים שניתן לשנות בתוך התצוגה בהתאמה אישית. כך אפשר להפוך את תצוגת 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
בהתאמה אישית.
זו פחות בעיה בכתיבה עם תכנים קומפוזביליים לשימוש חוזר. ההורים יכולים לציין בקלות מצב וקריאות חוזרות (callback), כך שאפשר לכתוב תכנים קומפוזביליים לשימוש חוזר, בלי לדעת איפה בדיוק ישתמשו בהם.
@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 קל הרבה יותר לשנות פריסות לגמרי בקוד באמצעות לוגיקה מותנית רגילה. מידע נוסף זמין במאמר שימוש בקטגוריות של גודל חלון.
בנוסף, במאמר תמיכה בגודלי מסכים שונים אפשר לקרוא על השיטות שבהן אפשר להיעזר בניסוח האוטומטי כדי ליצור ממשקי משתמש מותאמים.
גלילה בתצוגות עץ באמצעות 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
- כניסות חלון לכתיבה