1. מבוא
בשיעור ה-Codelab הזה תלמדו איך לפתח אפליקציות מותאמות לטלפונים, לטאבלטים ולמכשירים מתקפלים, ואיך לשפר את יכולת ההגעה אליהן באמצעות Jetpack Compose. תלמדו גם שיטות מומלצות לשימוש ברכיבים של Material 3 ובבחירת נושאים.
לפני שנתעמק, חשוב להבין את המשמעות של התאמה לפלטפורמות הפרסום.
התאמה
ממשק המשתמש של האפליקציה צריך להיות רספונסיבי, כך שיתאימו לחלונות בגדלים שונים, בכיוונים שונים ובגורמי צורה שונים. הפריסה הדינמית משתנה בהתאם לשטח המסך הזמין לה. השינויים האלה כוללים מגוון אפשרויות, החל מהתאמות פשוטות של פריסה כדי למלא מקום, בחירה של סגנונות הניווט המתאימים ועד שינוי של פריסות מתקדמות לניצול חדר נוסף.
מידע נוסף זמין במאמר עיצוב מותאם.
בשיעור ה-Codelab הזה, תלמדו איך להשתמש ב-Jetpack פיתוח נייטיב ולחשוב על יכולת ההתאמה. אתם מפתחים אפליקציה שנקראת 'תשובה'. האפליקציה הזו מדגימה איך להטמיע אפשרויות התאמה לכל סוגי המסכים, ואיך תכונות ההתאמה ופוטנציאל החשיפה פועלות יחד כדי לספק למשתמשים חוויה אופטימלית.
מה תלמדו
- איך לעצב את האפליקציה כך שתטרגט לכל הגדלים של החלונות באמצעות Jetpack Compose.
- איך לטרגט את האפליקציה למכשירים מתקפלים שונים.
- איך להשתמש בסוגים שונים של ניווט לשיפור הנגישות והנגישות.
- איך להשתמש ברכיבי Material 3 כדי לספק את החוויה הטובה ביותר בכל גודל חלון.
מה נדרש
- את הגרסה היציבה האחרונה של Android Studio.
- מכשיר וירטואלי שניתן לשנות את גודלו של Android 13.
- הידע של Kotlin.
- הבנה בסיסית של 'כתיבה' (למשל, ההערה
@Composable
). - היכרות בסיסית עם פריסות הכתיבה (למשל
Row
ו-Column
). - היכרות בסיסית עם מגבילי התאמה (למשל
Modifier.padding()
).
ב-Codelab הזה משתמשים באמולטור שניתן לשנות את גודלו, שמאפשר לעבור בין סוגים שונים של מכשירים וגדלים של חלונות.
אם אתם לא מכירים את התכונה 'כתיבה', מומלץ לעבור לקוד Lab הבסיסי של Jeetpack Compose לפני השלמת ה-Codelab הזה.
מה תפַתחו
- אפליקציית אימייל אינטראקטיבית בשם 'תשובה' שמשתמשת בשיטות מומלצות לעיצובים שניתנים להתאמה, לניווטים שונים ב-Material ולשימוש אופטימלי בשטח המסך.
2. להגדרה
כדי לקבל את הקוד של ה-Codelab הזה, משכפלים את מאגר GitHub משורת הפקודה:
git clone https://github.com/android/codelab-android-compose.git cd codelab-android-compose/AdaptiveUiCodelab
לחלופין, אפשר להוריד את המאגר כקובץ ZIP:
אנחנו ממליצים להתחיל עם הקוד בהסתעפות הראשי, ולפעול לפי ההוראות המפורטות ב-Codelab בקצב שלכם.
פתיחת הפרויקט ב-Android Studio
- בחלון Welcome to Android Studio, בוחרים באפשרות
Open an Existing Project.
- בוחרים את התיקייה
<Download Location>/AdaptiveUiCodelab
(חשוב להקפיד לבחור את הספרייהAdaptiveUiCodelab
שמכילה אתbuild.gradle
). - אחרי שמערכת Android Studio ייבאת את הפרויקט, כדאי לבדוק אם אפשר להריץ את ההסתעפות
main
.
בדיקת קוד ההתחלה
קוד ההסתעפות הראשי מכיל את החבילה ui
. החבילה הזו מאפשרת לעבוד עם הקבצים הבאים:
MainActivity.kt
– הפעילות של נקודת הכניסה שבה אתם מפעילים את האפליקציה.ReplyApp.kt
– מכיל תכנים קומפוזביליים של ממשק המשתמש במסך הראשי.ReplyHomeViewModel.kt
– מספק את הנתונים ואת מצב ממשק המשתמש של תוכן האפליקציה.ReplyListContent.kt
– מכיל תכנים קומפוזביליים שמספקים רשימות ומסכים של פרטים.
אם הפעלת את האפליקציה באמצעות אמולטור שניתן לשנות את גודלו ולנסות סוגים שונים של מכשירים, כמו טלפון או טאבלט, ממשק המשתמש פשוט מתרחב אל השטח הנתון במקום לנצל את שטח המסך או לספק ארגונומית של יכולת הגעה.
המערכת תעדכן אותו כדי לנצל את שטח המסך, להגביר את נוחות השימוש ולשפר את חוויית המשתמש הכוללת.
3. הגדרת אפליקציות כך שניתן יהיה להתאים אותן
בקטע הזה אנחנו מסבירים מה המשמעות של הפיכת אפליקציות לניתנות להתאמה, ואילו רכיבים אפשר להשתמש ב-Material 3 כדי לעשות את זה בקלות. הוא כולל גם את סוגי המסכים והמצבים שאליהם צריך לטרגט, כולל טלפונים, טאבלטים, טאבלטים גדולים ומכשירים מתקפלים.
תחילת העבודה על היסודות של גדלי חלונות, תנוחות קיפול וסוגים שונים של אפשרויות ניווט. לאחר מכן תוכלו להשתמש בממשקי ה-API האלה באפליקציה כדי להפוך אותה לגמישה יותר.
גודלי חלונות
מכשירי Android מגיעים בכל הצורות והגדלים, מטלפונים ועד מכשירים מתקפלים ועד טאבלטים ומכשירי ChromeOS. כדי לתמוך בכמה שיותר גדלים של חלונות, ממשק המשתמש שלכם צריך להיות רספונסיבי וגמיש. כדי לעזור לכם למצוא את הסף המתאים לשינוי ממשק המשתמש של האפליקציה, הגדרנו ערכים של נקודות עצירה (breakpoint) שעוזרים לסווג מכשירים לפי קטגוריות גדלים מוגדרות מראש (קומפקטית, בינונית ומורחבת), שנקראות סיווגי גודל של חלון. אלו הן קבוצה של נקודות עצירה (breakpoint) מקובעות באזור התצוגה. הן עוזרות לעצב, לפתח ולבדוק פריסות של אפליקציות רספונסיביות ומותאמות.
הקטגוריות נבחרו במיוחד כדי לאזן בין פשטות הפריסה, לבין הגמישות לבצע אופטימיזציה של האפליקציה לטיפול במקרים ייחודיים. סיווג גודל החלון נקבע תמיד לפי שטח המסך הזמין לאפליקציה, שלא יכול להיות כל המסך הפיזי לריבוי משימות או לפילוחים אחרים.
גם הרוחב וגם הגובה מסווגים בנפרד, כך שבכל נקודת זמן יש לאפליקציה שתי סיווגי גודל של חלונות: אחת לרוחב ואחד לגובה. בדרך כלל, הרוחב הזמין חשוב יותר מהגובה הזמין, בשל שפע הגלילה האנכית, לכן במקרה הזה משתמשים גם במחלקות של מידות הרוחב.
מצבי קיפול
במכשירים מתקפלים יש עוד יותר מצבים שבהם האפליקציה יכולה להסתגל, בגלל הגדלים השונים שלהם ונוכחות של צירים. צירים יכולים להסתיר חלק מהמסך, כך שהאזור לא יתאים להצגת תוכן. יכול להיות שגם כן יש הפרדה כזו, כלומר יש שני מסכים פיזיים נפרדים כשהמכשיר לא מקופל.
כמו כן, המשתמש עשוי להביט במסך הפנימי בזמן שהציר פתוח חלקית, וכתוצאה מכך נמצאים תנוחות פיזיות שונות על סמך כיוון הקיפול: יציבה שולחנית (קיפול אופקי, מופיע מימין בתמונה שלמעלה) ומצב הספר (קיפול אנכי).
מידע נוסף על קיפולים ותנוחות צירים.
כל הדברים האלה נכונים כשמטמיעים פריסות מותאמות שתומכות במכשירים מתקפלים.
קבלת מידע מותאם
הספרייה Material3 adaptive
מספקת גישה נוחה למידע על החלון שבו האפליקציה פועלת.
- מוסיפים רשומות של פריט המידע הזה שנוצר בתהליך הפיתוח (Artifact) והגרסה שלו לקובץ קטלוג הגרסאות:
gradle/libs.versions.toml
[versions]
material3Adaptive = "1.0.0-beta01"
[libraries]
androidx-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "material3Adaptive" }
- בקובץ ה-build של מודול האפליקציה, מוסיפים את התלות החדשה של הספרייה ולאחר מכן מבצעים סנכרון של Gradle:
app/build.gradle.kts
dependencies {
implementation(libs.androidx.material3.adaptive)
}
עכשיו, בכל היקף קומפוזבילי, אפשר להשתמש בפונקציה currentWindowAdaptiveInfo()
כדי לקבל אובייקט WindowAdaptiveInfo
שמכיל מידע כמו סיווג גודל החלון הנוכחי, ולבדוק אם המכשיר נמצא במצב מתקפל כמו במצב שולחני.
אפשר לנסות את זה עכשיו באפליקציית MainActivity
.
- ב-
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. ניווט דינמי
עכשיו הניווט באפליקציה ישתנה בהתאם לשינויים במצב המכשיר ובגודל שלו, כדי שיהיה קל יותר להשתמש באפליקציה.
כשמשתמשים מחזיקים טלפון, בדרך כלל האצבעות שלהם נמצאות בחלק התחתון של המסך. כשמשתמשים מחזיקים טאבלט או מכשיר מתקפל פתוח, האצבעות שלהם בדרך כלל קרובות לצדדים. למשתמשים צריכה להיות אפשרות לנווט או ליזום אינטראקציה עם אפליקציה בלי לדרוש תנוחות קיצוניות או לשנות את מיקום היד שלהם.
בזמן שאתם מתכננים את האפליקציה ומחליטים איפה למקם רכיבים אינטראקטיביים של ממשק המשתמש בפריסה, חשוב להביא בחשבון את ההשלכות הארגונומיות של אזורים שונים במסך.
- לאילו אזורים נוח להגיע בזמן שמחזיקים את המכשיר?
- לאילו אזורים ניתן להגיע רק באמצעות הרחבת אצבעות, דבר שעלול להיות לא נוח?
- לאילו אזורים קשה להגיע אל המכשיר, או באילו אזורים המשתמש מחזיק את המכשיר?
ניווט הוא הדבר הראשון שהמשתמשים מקיימים איתו אינטראקציה, והוא כולל פעולות חשובות שקשורות לתהליכים חיוניים שעוברים המשתמשים, ולכן כדאי למקם אותו באזורים שהכי קל להגיע אליהם. הספרייה 'חומר' דינמית שמיועדת כוללת מספר רכיבים שעוזרים להטמיע את הניווט, בהתאם לסוג גודל החלון של המכשיר.
ניווט בחלק התחתון
ניווט בחלק התחתון הוא מושלם לגדלים קומפקטיים, מכיוון שאנחנו מחזיקים את המכשיר באופן טבעי במקום שבו האגודל יכול להגיע בקלות לכל נקודות המגע התחתונות בניווט. אפשר להשתמש בו בכל פעם שיש לכם מכשיר קומפקטי או מכשיר מתקפל במצב מקופל וקומפקטי.
פס ניווט
לגודל חלון ברוחב בינוני, מסילת הניווט היא אידיאלית ליכולת הגעה כאשר האגודל שלנו נופל באופן טבעי בצד המכשיר. אפשר גם לשלב בין רכבת ניווט לחלונית הזזה לניווט כדי להציג מידע נוסף.
חלונית הזזה לניווט
חלונית ההזזה לניווט מספקת דרך קלה להציג מידע מפורט לגבי כרטיסיות ניווט, וניתן לגשת אליה בקלות כשמשתמשים בטאבלטים או במכשירים גדולים יותר. יש שני סוגים של חלונית הזזה לניווט: חלונית ההזזה לניווט וחלונית הזזה קבועה לניווט.
חלונית הזזה לניווט
ניתן להשתמש בחלונית ההזזה לניווט מודולרית לטלפונים ולטאבלטים קומפקטיים עד בינוניים, כי ניתן להרחיב או להסתיר אותה כשכבת-על בתוכן. לפעמים אפשר לשלב את האפשרות הזו עם רכבת ניווט.
חלונית הזזה קבועה לניווט
אתם יכולים להשתמש בחלונית ההזזה לניווט קבועה כדי לנווט באופן קבוע בטאבלטים גדולים, במכשירי Chromebook ובמחשבים שולחניים.
הטמעת ניווט דינמי
עכשיו ניתן לעבור בין סוגי ניווט שונים בהתאם לשינויים במצב המכשיר ובגודל שלו.
נכון לעכשיו, האפליקציה תמיד מציגה NavigationBar
מתחת לתוכן המסך, בלי קשר למצב המכשיר. במקום זאת, אתם יכולים להשתמש ברכיב 'חומר' NavigationSuiteScaffold
כדי לעבור באופן אוטומטי בין רכיבי הניווט השונים על סמך מידע כמו סיווג גודל החלון הנוכחי.
- מוסיפים את התלות של 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)
}
- מחפשים את הפונקציה הקומפוזבילית
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
. עם זאת, אפשר לעשות זאת באמצעות שינוי קטן.
- צריך להוסיף את הקוד הבא ממש לפני השיחה ל-
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
.
- כדי לקבל את הרכיב הזה, מוסיפים את יחסי התלות הבאים ומבצעים סנכרון של 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)
}
- צריך למצוא את הפונקציה הקומפוזבילית
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
.
- פותחים את
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
)
- באותו קובץ, יש לפונקציה
ReplyHomeViewModel
פונקצייתsetSelectedEmail()
שמופעלת כשהמשתמש מקיש על פריט ברשימה. משנים את הפונקציה הזו כדי להעתיק את המצב של ממשק המשתמש ולתעד את האימייל שנבחר:
ReplyHomeViewModel.kt
fun setSelectedEmail(email: Email) {
_uiState.update {
it.copy(selectedEmail = email)
}
}
כדאי להביא בחשבון את מה שקורה לפני שהמשתמש הקיש על פריט כלשהו וכתובת האימייל שנבחרה היא null
. מה יש להציג בחלונית הפרטים? יש כמה דרכים לטפל במקרה הזה, למשל הצגת הפריט הראשון ברשימה כברירת מחדל.
- באותו הקובץ, משנים את הפונקציה
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()
)
}
}
}
- חוזרים אל
ReplyApp.kt
ומשתמשים בהודעת האימייל שנבחרה, אם היא זמינה, כדי לאכלס את התוכן של חלונית הפרטים:
ReplyApp.kt
ListDetailPaneScaffold(
// ...
detailPane = {
if (replyHomeUIState.selectedEmail != null) {
ReplyDetailPane(replyHomeUIState.selectedEmail)
}
}
)
מריצים שוב את האפליקציה ומחליפים את האמולטור לגודל טאבלט, ורואים שהקשה על פריט ברשימה מעדכנת את התוכן של חלונית הפרטים.
זה מעולה כששתי החלוניות גלויות, אבל כשבחלון יש רק מקום להציג חלונית אחת, נראה ששום דבר לא קורה כשמקישים על פריט. כדאי לנסות לשנות את תצוגת האמולטור לטלפון או למכשיר מתקפל במצב תצוגה לאורך, ולראות שרק חלונית הרשימה גלויה גם אחרי שמקישים על פריט. הסיבה לכך היא שלמרות שכתובת האימייל שנבחרה מתעדכנת, המיקוד ב-ListDetailPaneScaffold
נשאר בחלונית הרשימה בהגדרות האלה.
- כדי לפתור את הבעיה, צריך להזין את הקוד הבא בתור ה-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
. בחלונות קטנים יותר, יוצג המראה שהאפליקציה ניווטה קדימה.
האפליקציה צריכה לטפל גם במה שקורה כשהמשתמש לוחץ על לחצן 'הקודם' מחלונית הפרטים. ההתנהגות הזו תשתנה בהתאם להצגה של חלונית אחת או שתי חלוניות.
- כדי לתמוך בניווט חזרה, כדאי להוסיף את הקוד הבא.
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.