פיתוח אפליקציות מותאמות באמצעות Jetpack Compose

1. מבוא

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

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

יכולת הסתגלות

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

מידע נוסף זמין במאמר בנושא עיצוב רספונסיבי.

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

מה תלמדו

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

הדרישות

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

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

אם אתם לא מכירים את Compose, מומלץ לעבור על ה-codelab בנושא היסודות של Jetpack Compose לפני שממשיכים עם ה-codelab הזה.

מה תפַתחו

  • אפליקציית תוכנת אימייל אינטראקטיבית בשם Reply, שמשתמשת בשיטות מומלצות לעיצובים שניתנים להתאמה, בניווטים שונים של Material ובשימוש אופטימלי בשטח המסך.

הדגמה של תמיכה בכמה מכשירים שתשיגו בסדנת הקוד הזו

2. להגדרה

כדי לקבל את הקוד של ה-Codelab הזה, משכפלים את מאגר GitHub משורת הפקודה:

git clone https://github.com/android/codelab-android-compose.git
cd codelab-android-compose/AdaptiveUiCodelab

אפשר גם להוריד את המאגר כקובץ ZIP:

מומלץ להתחיל עם הקוד בענף main ולפעול לפי השלבים ב-Codelab בקצב שלכם.

פתיחת הפרויקט ב-Android Studio

  1. בחלון Welcome to Android Studio (ברוכים הבאים ל-Android Studio), בוחרים באפשרות c01826594f360d94.pngOpen an Existing Project (פתיחת פרויקט קיים).
  2. בוחרים את התיקייה <Download Location>/AdaptiveUiCodelab (חשוב לבחור את הספרייה AdaptiveUiCodelab שמכילה את build.gradle).
  3. אחרי ש-Android Studio מייבא את הפרויקט, בודקים שאפשר להריץ את הענף main.

בדיקת קוד ההתחלה

הקוד בענף main מכיל את חבילת ui. תעבדו עם הקבצים הבאים בחבילה:

  • MainActivity.kt – פעילות של נקודת כניסה שבה מתחילים את האפליקציה.
  • ReplyApp.kt – מכיל רכיבי UI שניתנים להרכבה במסך הראשי.
  • ReplyHomeViewModel.kt – מספק את הנתונים ואת מצב ממשק המשתמש של תוכן האפליקציה.
  • ReplyListContent.kt – מכיל קומפוזיציות לאספקת רשימות ומסכי פרטים.

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

המסך הראשוני בטלפון

תצוגה ראשונית מורחבת בטאבלט

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

3. התאמת האפליקציות

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

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

גדלים של חלונות

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

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

‫WindowWidthSizeClass לרוחב קומפקטי, בינוני ומורחב.

‫WindowHeightSizeClass לגובה קומפקטי, בינוני ומורחב.

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

מצבי קיפול

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

מצבים של מכשירים מתקפלים: שטוח ופתוח בחצי

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

מידע נוסף על מצבי קיפול וצירים

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

קבלת מידע מותאם

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

  1. מוסיפים רשומות לארטיפקט הזה ולגירסה שלו לקובץ קטלוג הגרסאות:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0"

[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
  1. בקובץ ה-build של מודול האפליקציה, מוסיפים את התלות בספרייה החדשה ומבצעים סנכרון של Gradle:

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive)
}

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

אפשר לנסות את זה עכשיו ב-MainActivity.

  1. ב-onCreate() בתוך הבלוק ReplyTheme, מקבלים את המידע על ההתאמה של החלון ומציגים את מחלקות הגודל ב-Text composable. אפשר להוסיף את זה אחרי הרכיב ReplyApp():

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {
        ReplyTheme {
            val uiState by viewModel.uiState.collectAsStateWithLifecycle()
            ReplyApp(
                replyHomeUIState = uiState,
                onEmailClick = viewModel::setSelectedEmail
            )

            val adaptiveInfo = currentWindowAdaptiveInfo()
            val sizeClassText =
                "${adaptiveInfo.windowSizeClass.windowWidthSizeClass}\n" +
                "${adaptiveInfo.windowSizeClass.windowHeightSizeClass}"
            Text(
                text = sizeClassText,
                color = Color.Magenta,
                modifier = Modifier.padding(
                    WindowInsets.safeDrawing.asPaddingValues()
                )
            )
        }
    }
}

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

4. ניווט דינמי

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

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

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

  • באילו אזורים נוח להגיע כשמחזיקים את המכשיר?
  • באילו אזורים אפשר להגיע רק באמצעות מתיחת האצבעות, מה שיכול להיות לא נוח?
  • באילו אזורים קשה להגיע או שהם רחוקים מהמקום שבו המשתמש מחזיק את המכשיר?

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

ניווט בחלק התחתון

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

סרגל ניווט תחתון עם פריטים

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

פס ניווט עם פריטים

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

חלונית הזזה לניווט במצב מודאלי

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

חלונית הזזה לניווט במצב מודאלי עם פריטים

חלונית הזזה לניווט קבועה

אתם יכולים להשתמש בחלונית הזזה קבועה לניווט קבוע בטאבלטים גדולים, במכשירי Chromebook ובמחשבים.

חלונית הזזה לניווט עם פריטים

הטמעה של ניווט דינמי

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

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

  1. כדי להוסיף את הרכיב הזה, צריך לעדכן את קטלוג הגרסאות ואת סקריפט ה-build של האפליקציה, ואז לבצע סנכרון של Gradle:

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0"

[libraries]
androidx-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3AdaptiveNavSuite" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.navigation.suite)
}
  1. מחפשים את הפונקציה הניתנת להגדרה ReplyNavigationWrapper() ב-ReplyApp.kt ומחליפים את Column ואת התוכן שלו ב-NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    NavigationSuiteScaffold(
        navigationSuiteItems = {
            ReplyDestination.entries.forEach {
                item(
                    selected = it == selectedDestination,
                    onClick = { /*TODO update selection*/ },
                    icon = {
                        Icon(
                            imageVector = it.icon,
                            contentDescription = stringResource(it.labelRes)
                        )
                    },
                    label = {
                        Text(text = stringResource(it.labelRes))
                    },
                )
            }
        }
    ) {
        content()
    }
}

הארגומנט navigationSuiteItems הוא בלוק שמאפשר להוסיף פריטים באמצעות הפונקציה item(), בדומה להוספת פריטים ב-LazyColumn. בתוך ה-lambda האחורי, הקוד הזה קורא ל-content() שמועבר כארגומנט ל-ReplyNavigationWrapperUI().

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

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

  1. מוסיפים את הקוד הבא ממש לפני הקריאה לפונקציה NavigationSuiteScaffold:

ReplyApp.kt

@Composable
private fun ReplyNavigationWrapperUI(
    content: @Composable () -> Unit = {}
) {
    var selectedDestination: ReplyDestination by remember {
        mutableStateOf(ReplyDestination.Inbox)
    }

    val windowSize = with(LocalDensity.current) {
        currentWindowSize().toSize().toDpSize()
    }
    val layoutType = if (windowSize.width >= 1200.dp) {
        NavigationSuiteType.NavigationDrawer
    } else {
        NavigationSuiteScaffoldDefaults.calculateFromAdaptiveInfo(
            currentWindowAdaptiveInfo()
        )
    }

    NavigationSuiteScaffold(
        layoutType = layoutType,
        ...
    ) {
        content()
    }
}

הקוד הזה קודם מקבל את גודל החלון וממיר אותו ליחידות DP באמצעות currentWindowSize() ו-LocalDensity.current, ואז משווה את רוחב החלון כדי להחליט על סוג הפריסה של ממשק המשתמש של הניווט. אם רוחב החלון הוא לפחות 1200.dp, נעשה שימוש ב-NavigationSuiteType.NavigationDrawer. אחרת, המערכת תחזור לחישוב ברירת המחדל.

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

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

כל הכבוד, למדתם על סוגים שונים של ניווט שתומכים בסוגים שונים של גדלים ומצבים של חלונות.

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

5. שימוש בשטח המסך

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

ב-Material 3 מוגדרות שלוש פריסות קנוניות, שלכל אחת מהן יש הגדרות לגדלים קומפקטיים, בינוניים וגדולים של חלון. פריסת פרטי הרשימה הקנונית מושלמת לתרחיש השימוש הזה, והיא זמינה בכתיבת אימייל כ-ListDetailPaneScaffold.

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

gradle/libs.versions.toml

[libraries]
androidx-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout", version.ref = "material3Adaptive" }
androidx-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation", version.ref = "material3Adaptive" }

app/build.gradle.kts

dependencies {

    implementation(libs.androidx.material3.adaptive.layout)
    implementation(libs.androidx.material3.adaptive.navigation)
}
  1. מחפשים את הפונקציה הניתנת להגדרה ReplyAppContent() ב-ReplyApp.kt, שכרגע מציגה רק את חלונית הרשימה על ידי קריאה ל-ReplyListPane(). כדי להחליף את ההטמעה הזו ב-ListDetailPaneScaffold, מוסיפים את הקוד הבא. מכיוון שמדובר ב-API ניסיוני, מוסיפים גם את ההערה @OptIn לפונקציה ReplyAppContent():

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            ReplyListPane(replyHomeUIState, onEmailClick)
        },
        detailPane = {
            ReplyDetailPane(replyHomeUIState.emails.first())
        }
    )
}

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

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

שאר הפרמטרים הנדרשים הם ביטויי למדה שאפשר להרכיב עבור החלוניות. הלחצנים ReplyListPane() ו-ReplyDetailPane() (שנמצאים ב-ReplyListContent.kt) משמשים למילוי התפקידים של חלוניות הרשימה והפרטים, בהתאמה. הפונקציה ReplyDetailPane() מצפה לארגומנט של אימייל, ולכן בשלב הזה הקוד הזה משתמש באימייל הראשון מרשימת האימיילים ב-ReplyHomeUIState.

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

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

  1. פותחים את ReplyHomeViewModel.kt ומחפשים את סיווג הנתונים ReplyHomeUIState. מוסיפים מאפיין לאימייל שנבחר, עם ערך ברירת מחדל של null:

ReplyHomeViewModel.kt

data class ReplyHomeUIState(
    val emails : List<Email> = emptyList(),
    val selectedEmail: Email? = null,
    val loading: Boolean = false,
    val error: String? = null
)
  1. באותו קובץ, לרכיב ReplyHomeViewModel יש פונקציה setSelectedEmail() שמופעלת כשהמשתמש מקיש על פריט ברשימה. משנים את הפונקציה כדי להעתיק את מצב ממשק המשתמש ולתעד את האימייל שנבחר:

ReplyHomeViewModel.kt

fun setSelectedEmail(email: Email) {
    _uiState.update {
        it.copy(selectedEmail = email)
    }
}

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

  1. באותו קובץ, משנים את הפונקציה observeEmails(). כשנטענת רשימת האימיילים, אם לא נבחר אימייל במצב הקודם של ממשק המשתמש, מגדירים את האימייל הראשון:

ReplyHomeViewModel.kt

private fun observeEmails() {
    viewModelScope.launch {
        emailsRepository.getAllEmails()
            .catch { ex ->
                _uiState.value = ReplyHomeUIState(error = ex.message)
            }
            .collect { emails ->
                val currentSelection = _uiState.value.selectedEmail
                _uiState.value = ReplyHomeUIState(
                    emails = emails,
                    selectedEmail = currentSelection ?: emails.first()
                )
            }
    }
}
  1. חוזרים אל ReplyApp.kt ומשתמשים באימייל שנבחר, אם הוא זמין, כדי לאכלס את התוכן בחלונית הפרטים:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    detailPane = {
        if (replyHomeUIState.selectedEmail != null) {
            ReplyDetailPane(replyHomeUIState.selectedEmail)
        }
    }
)

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

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

  1. כדי לפתור את הבעיה, מוסיפים את הקוד הבא כ-lambda שמועבר אל ReplyListPane:

ReplyApp.kt

ListDetailPaneScaffold(
    // ...
    listPane = {
        ReplyListPane(
            replyHomeUIState = replyHomeUIState,
            onEmailClick = { email ->
                onEmailClick(email)
                navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
            }
        )
    },
    // ...
)

פונקציית ה-Lambda הזו משתמשת ב-Navigator שנוצר קודם כדי להוסיף התנהגות נוספת כשלוחצים על פריט. הפונקציה תקרא ל-lambda המקורי שהועבר לפונקציה הזו, ואז תקרא גם ל-navigator.navigateTo() ותציין איזו חלונית צריכה להיות מוצגת. לכל חלונית בפיגום משויך תפקיד, ובחלונית הפרטים התפקיד הוא ListDetailPaneScaffoldRole.Detail. בחלונות קטנים יותר, זה ייראה כאילו האפליקציה עברה קדימה.

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

  1. כדי להוסיף תמיכה בניווט אחורה, מוסיפים את הקוד הבא.

ReplyApp.kt

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ReplyAppContent(
    replyHomeUIState: ReplyHomeUIState,
    onEmailClick: (Email) -> Unit,
) {
    val navigator = rememberListDetailPaneScaffoldNavigator<Long>()

    BackHandler(navigator.canNavigateBack()) {
        navigator.navigateBack()
    }

    ListDetailPaneScaffold(
        directive = navigator.scaffoldDirective,
        value = navigator.scaffoldValue,
        listPane = {
            AnimatedPane {
                ReplyListPane(
                    replyHomeUIState = replyHomeUIState,
                    onEmailClick = { email ->
                        onEmailClick(email)
                        navigator.navigateTo(ListDetailPaneScaffoldRole.Detail, email.id)
                    }
                )
            }
        },
        detailPane = {
            AnimatedPane {
                if (replyHomeUIState.selectedEmail != null) {
                    ReplyDetailPane(replyHomeUIState.selectedEmail)
                }
            }
        }
    )
}

רכיב הניווט יודע את המצב המלא של ListDetailPaneScaffold, אם אפשר לנווט אחורה ומה צריך לעשות בכל התרחישים האלה. הקוד הזה יוצר BackHandler שמופעל בכל פעם שאפשר לנווט אחורה, ובתוך קריאות ה-lambda מופעל navigateBack(). בנוסף, כדי שהמעבר בין החלוניות יהיה חלק יותר, כל חלונית עטופה ב-AnimatedPane() composable.

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

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

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

6. מזל טוב

מעולה! סיימתם את ה-Codelab הזה ולמדתם איך ליצור אפליקציות שמותאמות לשינויים באמצעות Jetpack פיתוח נייטיב.

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

מה השלב הבא?

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

אפליקציות לדוגמה

מסמכי עזר