שינוי גודל של אפליקציות ל-Android

1. מבוא

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

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

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

מה תפַתחו

במהלך הסדנה נבחן את ההשלכות של שינוי גודל חופשי ונבצע אופטימיזציה של אפליקציית Android כדי להדגים שיטות מומלצות לשינוי גודל. האפליקציה שלכם:

יש לכם מניפסט תואם

  • הסרת הגבלות שמונעות שינוי חופשי של גודל האפליקציה

שמירה על המצב כשמשנים את הגודל

  • שומר על מצב ממשק המשתמש כשמשנים את הגודל באמצעות rememberSaveable
  • איך להימנע משכפול מיותר של עבודת רקע כדי לאתחל את ממשק המשתמש

הדרישות

  1. ידע ביצירה של אפליקציות Android בסיסיות
  2. ידע ב-ViewModel וב-State בפיתוח נייטיב
  3. מכשיר בדיקה שתומך בשינוי הגודל והמיקום של החלונות, כמו אחד מהמכשירים הבאים:

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

2. תחילת העבודה

משכפלים את המאגר מ-GitHub.

git clone https://github.com/android/large-screen-codelabs/

…או להוריד קובץ ZIP של המאגר ולחלץ אותו

ייבוא פרויקט

  • פתיחת Android Studio
  • בוחרים באפשרות Import Project (ייבוא פרויקט) או באפשרות File->New->Import Project (קובץ > חדש > ייבוא פרויקט).
  • עוברים למקום שבו שיבטתם או חילצתם את הפרויקט.
  • פותחים את התיקייה שינוי גודל.
  • פותחים את הפרויקט בתיקייה start. הקובץ הזה מכיל את קוד לתחילת הדרך.

לאפליקציה

  • איך יוצרים ומריצים את האפליקציה
  • כדאי לנסות לשנות את גודל האפליקציה

מה דעתך?

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

הגבלות במניפסט

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

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

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

3. שינויים בהגדרות של שינוי הגודל

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

מעקב אחרי שינויים בהגדרות

כדי לראות את השינויים האלה באפליקציה שנבנתה באמצעות מערכת התצוגה של Android, אפשר לבטל את View.onConfigurationChanged. ב-Jetpack פיתוח נייטיב, יש לנו גישה אל LocalConfiguration.current, שמתעדכן אוטומטית בכל פעם שמתבצעת קריאה אל View.onConfigurationChanged.

כדי לראות את השינויים בהגדרות באפליקציית הדוגמה, מוסיפים לאפליקציה רכיב שאפשר להרכיב ממנו ממשק משתמש שמציג ערכים מ-LocalConfiguration.current, או יוצרים פרויקט חדש לדוגמה עם רכיב כזה. דוגמה לממשק משתמש שבו אפשר לראות את הנתונים האלה:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

אפשר לראות דוגמה להטמעה בתיקיית הפרויקט observing-configuration-changes. כדאי לנסות להוסיף את הקוד הזה לממשק המשתמש של האפליקציה, להריץ אותו במכשיר הבדיקה ולראות את ממשק המשתמש מתעדכן כשההגדרה של האפליקציה משתנה.

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

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

4. רישום אירועים במחזור החיים של הפעילות

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

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

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

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

‫logcat שמציג את ה-methods של מחזור החיים של הפעילות שמופעלים כשמשנים את הגודל

עכשיו בודקים את Logcat כדי לראות אילו קריאות חוזרות (callback) של מחזור החיים של הפעילות מופעלות כשמשנים את גודל האפליקציה מהגודל הקטן ביותר האפשרי לגודל הגדול ביותר האפשרי

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

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

כדי להפחית את המורכבות שקשורה לשינויים בהגדרות, אפשר להשתמש בממשקי API ברמה גבוהה יותר, כמו WindowSizeClass, כדי להטמיע את ממשק המשתמש האדפטיבי. (ראו גם תמיכה בגדלים שונים של מסכים).

5. המשכיות – שמירה על המצב הפנימי של רכיבים הניתנים להרכבה כשמשנים את הגודל שלהם

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

מתחילים בכך שגורמים לפונקציה NavigationDrawerHeader הניתנת להגדרה (שנמצאת ב-ReplyHomeScreen.kt) להתרחב ולהציג את כתובת האימייל כשלוחצים עליה.

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

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

  1. מריצים את האפליקציה במכשיר הבדיקה
  2. מקישים על הכותרת כדי להרחיב אותה
  3. כדאי לנסות לשנות את גודל החלון

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

הכותרת בחלונית הזזה לניווט של האפליקציה מורחבת אחרי הקשה, אבל מצטמצמת אחרי שינוי הגודל של האפליקציה

מצב ממשק המשתמש אובד כי remember עוזר לשמור על המצב במהלך הרכבות מחדש, אבל לא במהלך פעילות או יצירה מחדש של תהליך. מקובל להשתמש בהעלאת הרמה של מצב (state hoisting), כלומר להעביר את המצב לקומפוזיציה שקוראת לקומפוזיציה אחרת, כדי שהקומפוזיציות יהיו בלי שמירת מצב. כך אפשר להימנע מהבעיה הזו לגמרי. עם זאת, אפשר להשתמש ב-remember במקומות שבהם שומרים את מצב רכיב ממשק המשתמש בתוך פונקציות שאפשר להרכיב.

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

6. איך נמנעים משכפול מיותר של עבודות ברקע

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

כדי לראות דוגמה לסוגי הבעיות שאתם עלולים להיתקל בהן, מוסיפים פקודת רישום ביומן לשיטה initializeUIState ב-ReplyViewModel.

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

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

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

כדי למנוע עבודה מיותרת ברקע, מסירים את הקריאה ל-initializeUIState() מהשיטה onCreate() של הפעילות. במקום זאת, מאתחלים את הנתונים בשיטה init של ViewModel. כך מוודאים ששיטת האתחול תפעל רק פעם אחת, כשיוצרים מופע ראשון של ReplyViewModel:

init {
    initializeUIState()
}

אפשר לנסות להפעיל את האפליקציה שוב, ולראות שמשימת האתחול המיותרת שנוצרה בהדמיה מופעלת רק פעם אחת, לא משנה כמה פעמים משנים את הגודל של חלון האפליקציה. הסיבה לכך היא ש-ViewModels נשמרים מעבר למחזור החיים של Activity. הפעלת קוד ההפעלה רק פעם אחת בזמן יצירת ViewModel מאפשרת לנו להפריד אותו מכל יצירה מחדש של Activity ולמנוע עבודה מיותרת. אם מדובר בקריאה יקרה לשרת או בפעולת קלט/פלט כבדה של קובץ כדי לאתחל את ממשק המשתמש, תוכלו לחסוך משאבים משמעותיים ולשפר את חוויית המשתמש.

7. איזה כיף!

כל הכבוד! כל הכבוד! הטמעתם עכשיו כמה שיטות מומלצות כדי לאפשר שינוי גודל של אפליקציות ל-Android ב-ChromeOS ובסביבות אחרות עם חלונות מרובים ומסכים מרובים.

קוד מקור לדוגמה

שיבוט המאגר מ-GitHub

git clone https://github.com/android/large-screen-codelabs/

…או להוריד קובץ ZIP של המאגר ולחלץ אותו