שיקולים נוספים

המעבר מ'תצוגות' ל'כתיבה' קשור רק לממשק המשתמש, אבל יש הרבה שצריך לקחת בחשבון כדי לבצע מיגרציה בטוחה ומצטברת. הזה מכילים שיקולים מסוימים, אפליקציה מבוססת-צפייה לכתיבה.

העברת העיצוב של האפליקציה

Material Design היא מערכת העיצוב המומלצת בשביל אפליקציות ל-Android.

לאפליקציות שמבוססות על צפייה יש שלוש גרסאות של Material:

  • Material Design 1 באמצעות ספריית AppCompat (כלומר Theme.AppCompat.*)
  • Material Design 2 באמצעות MDC-Android ספרייה (כלומר Theme.MaterialComponents.*)
  • Material Design 3 באמצעות MDC-Android ספרייה (כלומר Theme.Material3.*)

לאפליקציות פיתוח נייטיב יש שתי גרסאות של Material:

מומלץ להשתמש בגרסה העדכנית ביותר (Material 3) אם מערכת העיצוב של האפליקציה מסוגלת לעשות זאת. יש מדריכים להעברת נתונים לגבי שתי התצוגות וכתיבה:

כשיוצרים מסכים חדשים ב'כתיבה', ללא קשר לגרסת Material העיצוב שבו בחרת להשתמש, חשוב להקפיד להחיל MaterialTheme לפני תכנים קומפוזביליים שפולטים ממשק משתמש מהספריות 'הוספת חומרים'. החומר רכיבים (Button, Text וכו') תלויים בקיומו של MaterialTheme וההתנהגות שלהם לא מוגדרת בלעדיו.

הכול דוגמאות של Jetpack Compose להשתמש בעיצוב מותאם אישית של 'כתיבה' שמבוסס על MaterialTheme.

למידע נוסף, ראו עיצוב מערכות בכתיבה והעברת עיצובי XML לכתיבה.

אם אתם משתמשים ברכיב הניווט באפליקציה: בתפריט ניווט באמצעות 'כתיבה' – יכולת פעולה הדדית לקבלת מידע נוסף, אפשר להעביר את הניווט ב-Jetpack ל'פיתוח נייטיב'.

בדיקה של ממשק המשתמש המעורב של הכתיבה/התצוגות

אחרי ההעברה של חלקים מהאפליקציה לכתיבה, חשוב מאוד לבצע את הבדיקה כדי לוודא לא שברתם שום דבר.

כאשר פעילות או מקטע משתמשים ב'כתיבה', עליך להשתמש ב- 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()
    }
}

למידע נוסף על בדיקות, אפשר לעיין במאמר בנושא בדיקת פריסת הכתיבה. עבור של יכולת פעולה הדדית עם frameworks של בדיקות ממשק משתמש, יכולת פעולה הדדית עם Espresso יכולת פעולה הדדית עם UiAutomator.

שילוב של פיתוח נייטיב עם ארכיטקטורת האפליקציה הקיימת

ארכיטקטורה של זרימת נתונים חד-כיוונית (UDF) דפוסים, עובדים בצורה חלקה עם 'פיתוח נייטיב'. אם האפליקציה משתמשת בסוגים אחרים של של ארכיטקטורת 'מציג מודלים' (MVP), להעביר את החלק הזה בממשק המשתמש ל-UDF לפני או תוך כדי המעבר לכתיבה.

שימוש ב-ViewModel בכתיבה

אם אתם משתמשים ברכיבי הארכיטקטורה ViewModel, יש לך אפשרות לגשת ViewModel מכל תוכן קומפוזבילי מאת קוראים לפונקציה viewModel() כפי שמוסבר בכתיבה וספריות אחרות.

כשמתחילים לכתוב, חשוב לשים לב לשימוש באותו סוג 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") ?: "")
        }
    }
}

מצב מקור האמת

כאשר משתמשים בתכונה 'כתיבה' בחלק אחד של ממשק המשתמש, ייתכן שהאפשרות 'כתיבה' וגם קוד המערכת של View צריך לשתף נתונים. כשהדבר אפשרי, מומלץ כוללים את המצב המשותף במחלקה אחרת שפועלת לפי השיטות המומלצות של UDF בשימוש בשתי הפלטפורמות; לדוגמה, ב-ViewModel שחושף זרם של נתונים משותפים כדי להפיץ עדכוני נתונים.

עם זאת, זה לא תמיד אפשרי אם הנתונים שישותפו ניתנים לשינוי או שמקושרת חזק לרכיב בממשק המשתמש. במקרה זה, מערכת אחת חייבת להיות המקור של האמת, והמערכת צריכה לשתף עדכוני נתונים עם המערכת השנייה. בתור ככלל, מקור האמת צריך להיות בבעלות של כל רכיב הוא קרוב יותר לרמה הבסיסית (root) של היררכיית ממשק המשתמש.

כתוב כמקור האמת

משתמשים ב SideEffect קומפוזבילי לפרסום מצב הכתיבה לקוד שאינו כתיבה. במקרה הזה, הפרמטר מקור האמת נשמר בתוכן קומפוזבילי, ששולח עדכוני מצב.

לדוגמה, ספריית ניתוח הנתונים עשויה לאפשר לכם לפלח את המשתמשים אוכלוסייה על ידי צירוף מטא נתונים מותאמים אישית (מאפייני משתמש בדוגמה הזו) לכל האירועים הבאים של ניתוח נתונים. כדי לציין את סוג המשתמש של המשתמש הנוכחי בספריית ניתוח הנתונים, צריך להשתמש בפרמטר 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
}

למידע נוסף, ראו תופעות לוואי ב'כתיבה'.

צפייה במערכת כמקור האמת

אם המערכת של התצוגה היא הבעלים של המצב והיא משתפת אותו עם 'כתיבה', מומלץ צריך לתחום את המצב ב-mutableStateOf אובייקטים כדי שהוא יהיה בטוח לשרשורים פיתוח נייטיב. אם משתמשים בגישה הזו, פונקציות קומפוזביליות פשוטות יותר כי כבר אין להם את מקור האמת, אבל מערכת View צריכה לעדכן ניתן לשינוי ואת התצוגות המפורטות שמשתמשות במצב הזה.

בדוגמה הבאה, 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 שהוחלף, למקם את התוכן הקומפוזבילי שיצרתם עטוף בעיצוב של הכתיבה, כמו שמוצג דוגמה:

@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 לכתיבה, צריך להפריד את הנתונים שמעובדים יצירת זרימת נתונים חד-כיוונית, כפי שמוסבר בפירוט בקטע העלאת מצב של מדינה.

לדוגמה, ל-View יש מאפיין visibility שמתאר אם הוא גלוי, לא נראה או נעלם. זהו מאפיין מובנה של View. בזמן קטעי קוד אחרים עשויים לשנות את החשיפה של View, אלא רק של View עצמו יודע באמת מהי החשיפה הנוכחית שלו. הלוגיקה שצריך להבטיח View גלוי יכול להיות מועד לשגיאות, והוא קשור בדרך כלל לView עצמו.

לעומת זאת, התכונה 'פיתוח נייטיב' מאפשרת להציג בקלות תכנים קומפוזביליים שונים לחלוטין באמצעות לוגיקה מותנית ב-Kotlin:

@Composable
fun MyComposable(showCautionIcon: Boolean) {
    if (showCautionIcon) {
        CautionIcon(/* ... */)
    }
}

בזכות העיצוב, CautionIcon לא צריך לדעת למה הוא מוצג או שלא אכפת לו ממנו, ואין קונספט של visibility: או שזה ביצירה, או היא לא.

הפרדה ברורה בין ניהול המצבים לבין לוגיקת המצגת, מאפשרת לכם לשנות באופן חופשי את האופן שבו אתם מציגים תוכן כהמרה של מצב לממשק משתמש. להיות היכולת להעלות את המצב במקרה הצורך הופכת תכנים קומפוזביליים לשימוש חוזר, בעלות על המדינה גמישה יותר.

קידום רכיבים מורכבים לשימוש חוזר

לרוב, לרכיבי 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 רספונסיביות. מקורות מידע שעומדים בדרישות עדיין זמינים להחלטות בנוגע לפריסה ברמת המסך, הרבה יותר קל לשנות בעזרת 'כתיבה' לגמרי בקוד עם לוגיקה מותנית רגילה. למידע נוסף, ראו סיווגים של גודל חלון.

בנוסף, כדאי לעיין במאמר תמיכה בגודלי מסכים שונים ללמוד על הטכניקות שכותבים הצעות לפיתוח של ממשקי משתמש מותאמים.

גלילה בתוך עץ עם 'תצוגות'

למידע נוסף על האופן שבו מפעילים יכולת פעולה הדדית של גלילה מקוננת בין רכיבי תצוגה שניתנים לגלילה ותכנים קומפוזביליים שניתנים לגלילה, המוצבים בשני הכיוונים, הקראה יכולת פעולה הדדית של גלילה בתוך גלילה.

כתיבת הודעה בRecyclerView

תכנים קומפוזביליים ב-RecyclerView יש ביצועים טובים החל מגרסה RecyclerView 1.3.0-alpha02. צריך לוודא שאתם משתמשים בגרסה 1.3.0-alpha02 לפחות של RecyclerView כדי לראות את ההטבות האלה.

יכולת פעולה הדדית של WindowInsets עם תצוגות

ייתכן שיהיה עליך לעקוף רכיבי inset שמוגדרים כברירת מחדל כשבמסך יש גם את האפשרות 'תצוגות' וגם את כתיבת הקוד באותה היררכיה. במקרה הזה צריך לציין טקסט באופן מפורש איזה מהם צריך לצרוך את הרכיבים הפנימיים, ומאיזה מהם צריך להתעלם.

לדוגמה, אם הפריסה החיצונית ביותר שלך היא פריסת Android View, עליך צורכים את הרכיבים הפנימיים במערכת התצוגה ומתעלמים מהם לצורך פיתוח נייטיב. לחלופין, אם הפריסה החיצונית ביותר שלכם היא קומפוזבילית, צריך לצרוך את inset ב-Compose (כתיבה), ומרפד את התכנים הקומפוזביליים של AndroidView בהתאם.

כברירת מחדל, כל ComposeView צורך את כל הרכיבים הפנימיים רמת הצריכה של WindowInsetsCompat. כדי לשנות את התנהגות ברירת המחדל, צריך להגדיר ComposeView.consumeWindowInsets אל false.

לקבלת מידע נוסף, אפשר לעיין במסמכי התיעוד של WindowInsets בקטע 'כתיבה'.