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

1. מבוא

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

לפני שנמשיך, חשוב להבין מה אנחנו מתכוונים במונח 'התאמה'.

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

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

מידע נוסף זמין במאמר עיצוב מותאם.

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

מה תלמדו

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

מה נדרש

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

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

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

מה תפַתחו

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

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

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, בוחרים באפשרות c01826594f360d94.pngOpen an Existing Project.
  2. בוחרים את התיקייה <Download Location>/AdaptiveUiCodelab (חשוב לוודא שבוחרים את הספרייה AdaptiveUiCodelab שמכילה את build.gradle).
  3. אחרי ש-Android Studio תייבא את הפרויקט, צריך לבדוק אם אפשר להריץ את ההסתעפות main.

הסבר על קוד ההתחלה

קוד ההסתעפות main מכיל את החבילה ui. החבילה הזו מאפשרת לעבוד עם הקבצים הבאים:

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

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

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

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

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

3. הגדרת אפליקציות כך שניתן יהיה להתאים אותן

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

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

גדלי חלונות

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

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

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

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

בחומר 3 (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())
        }
    )
}

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

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

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

ReplyApp.kt

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

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

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

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

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

6. מזל טוב

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

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

מה השלב הבא?

כדאי לעיין במדריכי Codelab האחרים במסלול Compose.

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

  • הדוגמאות ל-Compose הן אוסף של אפליקציות רבות שמשלבות את השיטות המומלצות שמפורטות ב-codelabs.

מסמכי עזר