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

‫1. מבוא

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

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

התאמה

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

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

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

מה תלמדו

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

מה הדרישות כדי להצטרף לתוכנית?

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

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

אם אתם לא מכירים את התכונה 'כתיבה', מומלץ לעבור ל-codelab ב-Jetpack פיתוח נייטיב לפני השלמת ה-Codelab הזה.

מה תפַתחו

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

תצוגה של תמיכה במכשירים מרובים שתוכלו להשיג ב-Codelab הזה

‫2. להגדרה

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

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

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

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

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

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

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

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

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

בשלב הראשון המערכת תתמקד ב-MainActivity.kt.

MainActivity.kt

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

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

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

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

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

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

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

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

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

גודלי חלונות

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

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

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

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

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

מצבי קיפול

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

תנוחות מתקפלות, שטוחות ופתוחות למחצה

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

מידע נוסף על קיפולים ותנוחות צירים.

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

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

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

  1. מוסיפים רשומות של פריט המידע הזה שנוצר בתהליך הפיתוח (Artifact) והגרסה שלו לקובץ קטלוג הגרסאות:

gradle/libs.versions.toml

[versions]
material3Adaptive = "1.0.0-beta01"

[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(20.dp)
            )
        }
    }
}

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

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

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

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

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

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

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

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

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

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

רכבת ניווט עם פריטים

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

חלונית הזזה לניווט

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

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

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

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

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

הטמעת ניווט דינמי

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

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

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

gradle/libs.versions.toml

[versions]
material3AdaptiveNavSuite = "1.3.0-beta01"

[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

[versions]
material3Adaptive = "1.0.0-beta01"

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

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

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

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

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

6. מזל טוב

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

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

מה השלב הבא?

אתם יכולים לנסות את שאר ה-codelabs בקטע נתיב לכתיבה.

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

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

מסמכי עזר