פיתוח ניווט רספונסיבי

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

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

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

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

ניווט בממשק משתמש רספונסיבי

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

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

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

<!-- res/layout/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w600dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigationrail.NavigationRailView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

<!-- res/layout-w1240dp/main_activity.xml -->

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.navigation.NavigationView
        android:layout_width="wrap_content"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        ... />

    <!-- Content view(s) -->
</androidx.constraintlayout.widget.ConstraintLayout>

יעדים של תוכן רספונסיבי

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

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

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

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

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

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

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

יעדים של תוכן עם פריסות חלופיות

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

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

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

<!-- Single destination for list and detail. -->

<navigation ...>

    <!-- Fragment that implements SlidingPaneLayout. -->
    <fragment
        android:id="@+id/article_two_pane"
        android:name="com.example.app.ListDetailTwoPaneFragment" />

    <!-- Other destinations... -->
</navigation>

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

תרשים ניווט אחד

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

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

מארח ניווט בתצוגת עץ

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

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

<!-- layout/two_pane_fragment.xml -->

<androidx.slidingpanelayout.widget.SlidingPaneLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/sliding_pane_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/list_pane"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start"/>

    <!-- Detail pane is a nested navigation host. Its graph is not connected
         to the main graph that contains the two_pane_fragment destination. -->
    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/detail_pane"
        android:layout_width="300dp"
        android:layout_weight="1"
        android:layout_height="match_parent"
        android:name="androidx.navigation.fragment.NavHostFragment"
        app:navGraph="@navigation/detail_pane_nav_graph" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>

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

מידע נוסף זמין במאמר תרשימי ניווט בתצוגת עץ.

מצב ששמור

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

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

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

היקפי ViewModel

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

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

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

כדי לשתף את מצב ממשק המשתמש בין קטעי הקוד, צריך להגדיר את ViewModel לפעילות באמצעות קריאה ל-activityViewModels() בקטעי הקוד (המקביל ל-Activity הוא פשוט viewModels()). כך הפעילות וכל קטעי הקוד שמצורפים אליה יכולים לשתף את המופע של ViewModel. עם זאת, בארכיטקטורה של פעילות אחת, ההיקף של ViewModel נמשך בפועל כל עוד האפליקציה פועלת, כך שה-ViewModel נשאר בזיכרון גם אם אף קטע לא משתמש בו.

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

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

היקף הענקת גישה לנכס אפשר לשתף את ViewModel עם
מקטע (fragment) Fragment.viewModels() קטע קוד בלבד
פעילות Activity.viewModels() או Fragment.activityViewModels() הפעילות וכל הקטעים שמצורפים אליה
תרשים ניווט Fragment.navGraphViewModels() כל הקטעים נמצאים באותו תרשים ניווט

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

מקורות מידע נוספים