תמיכה בגדלים שונים של מסכים מאפשרת גישה לאפליקציה ממגוון רחב של מכשירים וממספר גדול יותר של משתמשים.
כדי לתמוך בכמה שיותר גדלים של מסכים – בין אם מדובר במסכים של מכשירים שונים או בחלונות שונים של אפליקציות במצב מרובה חלונות – צריך לעצב את פריסות האפליקציה כך שיהיו רספונסיביות וניתנות להתאמה. פריסות רספונסיביות/דינמיות מספקות חוויית משתמש אופטימלית ללא קשר לגודל המסך, ומאפשרות לאפליקציה להתאים לטלפונים, לטאבלטים, למכשירים מתקפלים, למכשירי ChromeOS, לכיווני מסך לרוחב ולאורך ולתצורות מסך שניתן לשנות את הגודל שלהן, כמו מצב מסך מפוצל וחלונות במחשב.
פריסות רספונסיביות/אדפטיביות משתנות בהתאם לשטח הזמין להצגה. השינויים נעים בין התאמות קטנות בפריסה שממלאות את השטח (עיצוב רספונסיבי) לבין החלפה מלאה של פריסה אחת בפריסה אחרת, כדי שהאפליקציה תוכל להתאים את עצמה בצורה הטובה ביותר לגדלים שונים של מסכים (עיצוב אדפטיבי).
ערכת הכלים Jetpack Compose היא ערכת כלים הצהרתית לממשק משתמש, ולכן היא אידיאלית לעיצוב וליישום של פריסות שמשתנות באופן דינמי כדי להציג תוכן בצורה שונה בגדלים שונים של מסכים.
הגדרת שינויים משמעותיים בפריסה לרכיבים קומפוזביליים ברמת התוכן
רכיבי composable ברמת האפליקציה וברמת התוכן תופסים את כל שטח התצוגה שזמין לאפליקציה. בסוגים האלה של רכיבי composable, יכול להיות שכדאי לשנות את הפריסה הכוללת של האפליקציה במסכים גדולים.
אל תשתמשו בערכים של חומרה פיזית כדי לקבל החלטות לגבי פריסת הרכיבים. יכול להיות שתתפתו לקבל החלטות על סמך ערך קבוע ומוחשי (האם המכשיר הוא טאבלט? האם למסך הפיזי יש יחס רוחב-גובה מסוים?), אבל התשובות לשאלות האלה לא בהכרח יעזרו לכם לקבוע את השטח שזמין לממשק המשתמש.
בטאבלטים, יכול להיות שאפליקציה פועלת במצב ריבוי חלונות, כלומר האפליקציה עשויה לפצל את המסך עם אפליקציה אחרת. במצב שינוי דינמי של חלונות במחשב או ב-ChromeOS, יכול להיות שאפליקציה נמצאת בחלון שניתן לשינוי גודל. יכול להיות שיש יותר ממסך פיזי אחד, כמו במכשיר מתקפל. בכל המקרים האלה, גודל המסך הפיזי לא רלוונטי להחלטה איך להציג את התוכן.
במקום זאת, כדאי לקבל החלטות על סמך החלק בפועל של המסך שהוקצה לאפליקציה, כפי שמתואר במדדי החלון הנוכחיים שסופקו על ידי ספריית WindowManager של Jetpack. דוגמה לשימוש ב-WindowManager באפליקציית Compose מופיעה בדוגמה JetNews.
התאמת הפריסות לשטח המסך הזמין גם מפחיתה את כמות הטיפול המיוחד שנדרשת לתמיכה בפלטפורמות כמו ChromeOS ובגורמי צורה כמו טאבלטים ומכשירים מתקפלים.
אחרי שמגדירים את המדדים של השטח שזמין לאפליקציה, ממירים את הגודל הגולמי למחלקת גודל חלון כמו שמתואר במאמר שימוש במחלקות גודל חלון. נקודות עצירה של גודל החלון נועדו ליצור איזון בין פשטות הלוגיקה של האפליקציה לבין הגמישות שמאפשרת לבצע אופטימיזציה של האפליקציה לרוב גדלי המסכים.
גודל החלון מתייחס לחלון הכולל של האפליקציה, ולכן כדאי להשתמש במחלקות כדי לקבל החלטות לגבי פריסת הרכיבים שמשפיעות על הפריסה הכוללת של האפליקציה. אפשר להעביר את מחלקות הגודל של החלון כמצב, או לבצע לוגיקה נוספת כדי ליצור מצב נגזר להעברה למרכיבים הניתנים להרכבה מקוננים.
@Composable fun MyApp( windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass ) { // Decide whether to show the top app bar based on window size class. val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND) // MyScreen logic is based on the showTopAppBar boolean flag. MyScreen( showTopAppBar = showTopAppBar, /* ... */ ) }
גישה מבוססת-שכבות מגבילה את הלוגיקה של גודל התצוגה למיקום יחיד, במקום לפזר אותה באפליקציה במקומות רבים שצריך לשמור על סנכרון ביניהם. מיקום יחיד יוצר מצב, שאפשר להעביר אותו במפורש לרכיבי Composable אחרים, בדיוק כמו כל מצב אחר של האפליקציה. העברה מפורשת של מצב מפשטת את הקומפוזיציות האישיות, כי הקומפוזיציות מקבלות את מחלקת גודל החלון או את ההגדרה שצוינה יחד עם נתונים אחרים.
אפשר לעשות שימוש חוזר ברכיבים קומפוזביליים גמישים ומקוננים
רכיבים קומפוזביליים הם שימושיים יותר כשאפשר למקם אותם במגוון רחב של מקומות. אם רכיב קומפוזבילי צריך להיות ממוקם במיקום ספציפי בגודל ספציפי, סביר להניח שלא ניתן יהיה להשתמש בו מחדש בהקשרים אחרים. זה גם אומר שרכיבי composable נפרדים לשימוש חוזר לא צריכים להיות תלויים באופן מרומז במידע על גודל התצוגה הגלובלי.
דמיינו רכיב composable מוטמע שמיישם פריסת רשימה עם פרטים, שיכולה להציג חלונית אחת או שתי חלוניות זו לצד זו:
ההחלטה לגבי רשימה מפורטת צריכה להיות חלק מהפריסה הכוללת של האפליקציה, ולכן ההחלטה מועברת מרכיב שאפשר להרכיב ברמת התוכן:
@Composable fun AdaptivePane( showOnePane: Boolean, /* ... */ ) { if (showOnePane) { OnePane(/* ... */) } else { TwoPane(/* ... */) } }
מה אם רוצים שרכיב שאפשר להרכיב ישנה את הפריסה שלו באופן עצמאי על סמך שטח המסך שזמין, למשל כרטיס שבו מוצגים פרטים נוספים אם יש מקום? אתם רוצים לבצע פעולה לוגית כלשהי על סמך גודל המסך הזמין, אבל איזה גודל בדיוק?
אל תנסו להשתמש בגודל המסך בפועל של המכשיר. הערך הזה לא יהיה מדויק עבור סוגים שונים של מסכים, וגם לא יהיה מדויק אם האפליקציה לא מוצגת במסך מלא.
מכיוון שרכיב ה-Composable הוא לא רכיב Composable ברמת התוכן, אל תשתמשו במדדים של החלון הנוכחי באופן ישיר.
אם הרכיב ממוקם עם ריווח פנימי (padding) (למשל עם שוליים פנימיים), או אם האפליקציה כוללת רכיבים כמו פסי ניווט או סרגלי אפליקציות, כמות שטח התצוגה שזמין לרכיב הניתן להרכבה עשויה להיות שונה באופן משמעותי מהשטח הכולל שזמין לאפליקציה.
משתמשים ברוחב שמוקצה בפועל לרכיב הקומפוזבילי כדי לעבד את עצמו. יש שתי אפשרויות לקבל את הרוחב הזה:
אם רוצים לשנות איפה או איך התוכן מוצג, אפשר להשתמש באוסף של משנים או בפריסה בהתאמה אישית כדי שהפריסה תהיה רספונסיבית. האפשרויות הן פשוטות: אפשר למלא את כל השטח הפנוי בילדים, או לסדר את הילדים בכמה עמודות אם יש מספיק מקום.
אם רוצים לשנות מה מוצג, אפשר להשתמש ב-
BoxWithConstraintsכחלופה יעילה יותר. BoxWithConstraintsמספק אילוצים למדידה שבהם אפשר להשתמש כדי לקרוא לפונקציות שונות של קומפוזיציה בהתאם לשטח התצוגה הזמין. עם זאת, יש לכך מחיר מסוים, כיBoxWithConstraintsדוחה את הקומפוזיציה עד לשלב הפריסה, שבו המגבלות האלה ידועות, ולכן נדרשת יותר עבודה במהלך הפריסה.
@Composable fun Card(/* ... */) { BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(/* ... */) Title(/* ... */) } } else { Row { Column { Title(/* ... */) Description(/* ... */) } Image(/* ... */) } } } }
הפיכת כל הנתונים לזמינים לגדלים שונים של מסכים
כשמטמיעים קומפוזיציה שמנצלת את השטח הנוסף של המסך, יכול להיות שתתפתו להיות יעילים ולטעון נתונים כתוצאה לוואי של גודל המסך הנוכחי.
עם זאת, פעולה כזו מנוגדת לעיקרון של זרימת נתונים חד-כיוונית, שבה אפשר להעביר נתונים ולספק אותם לרכיבים הניתנים להרכבה כדי להציג אותם בצורה מתאימה. צריך לספק מספיק נתונים לרכיב שאפשר להרכיב כדי שלרכיב תמיד יהיה מספיק תוכן לכל גודל מסך, גם אם חלק מהתוכן לא תמיד ישמש.
@Composable fun Card( imageUrl: String, title: String, description: String ) { BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(imageUrl) Title(title) } } else { Row { Column { Title(title) Description(description) } Image(imageUrl) } } } }
בדוגמה Card, שימו לב שהערך description תמיד מועבר אל Card. למרות שהתג description משמש רק כשהרוחב מאפשר את התצוגה שלו, התג Card תמיד דורש את התג description, ללא קשר לרוחב הזמין.
העברת תוכן מספיק תמיד מפשטת את הפריסות הדינמיות, כי היא הופכת אותן לפחות תלויות במצב, ומונעת הפעלה של תופעות לוואי כשעוברים בין גדלי מסך (שיכולות להתרחש בגלל שינוי גודל החלון, שינוי הכיוון או קיפול ופתיחה של מכשיר).
העיקרון הזה מאפשר גם לשמור את המצב של הרכיבים כשמשנים את הפריסה. העברת מידע שלא בטוח שישמש בכל גדלי התצוגה מאפשרת לשמור על מצב האפליקציה כשגודל הפריסה משתנה.
לדוגמה, אפשר להעלות showMore דגל בוליאני כדי לשמור את מצב האפליקציה כששינוי הגודל של התצוגה גורם לפריסה לעבור בין הסתרה להצגה של התוכן:
@Composable fun Card( imageUrl: String, title: String, description: String ) { var showMore by remember { mutableStateOf(false) } BoxWithConstraints { if (maxWidth < 400.dp) { Column { Image(imageUrl) Title(title) } } else { Row { Column { Title(title) Description( description = description, showMore = showMore, onShowMoreToggled = { newValue -> showMore = newValue } ) } Image(imageUrl) } } } }
מידע נוסף
למידע נוסף על פריסות דינמיות ב-Compose, אפשר לעיין במקורות המידע הבאים:
אפליקציות לדוגמה
- CanonicalLayouts הוא מאגר של דפוסי עיצוב מוכחים שמספקים חוויית משתמש אופטימלית במסכים גדולים
- ב-JetNews מוצג איך לעצב אפליקציה שמתאימה את ממשק המשתמש שלה כדי לנצל את שטח התצוגה הזמין
- Reply היא דוגמה אדפטיבית לתמיכה בניידים, בטאבלטים ובמכשירים מתקפלים
- Now in Android היא אפליקציה שמשתמשת בפריסות דינמיות כדי לתמוך בגדלים שונים של מסכים
סרטונים