สร้างการนําทางที่ปรับเปลี่ยนตามอุปกรณ์

การไปยังส่วนต่างๆ คือการโต้ตอบของผู้ใช้กับ UI ของแอปพลิเคชันเพื่อเข้าถึงปลายทางของเนื้อหา หลักการในการนําทางของ Android มีหลักเกณฑ์ที่จะช่วยคุณสร้างการนําทางแอปที่สอดคล้องกันและใช้งานง่าย

UI ที่ปรับเปลี่ยนตามอุปกรณ์/ที่ปรับเปลี่ยนได้มีปลายทางเนื้อหาที่ปรับเปลี่ยนตามอุปกรณ์ และมักจะมีองค์ประกอบการนำทางประเภทต่างๆ เพื่อตอบสนองต่อการเปลี่ยนแปลงขนาดของจอแสดงผล เช่น แถบนําทางด้านล่างในจอแสดงผลขนาดเล็ก แถบนําทางในจอแสดงผลขนาดกลาง หรือลิ้นชักการนําทางแบบคงที่ในจอแสดงผลขนาดใหญ่ แต่ UI ที่ปรับเปลี่ยนตามอุปกรณ์/ที่ปรับเปลี่ยนได้ยังคงต้องเป็นไปตามหลักการนําทาง

คอมโพเนนต์การนำทางของ Jetpack ใช้หลักการในการนําทางและช่วยพัฒนาแอปด้วย UI แบบตอบสนอง/ปรับเปลี่ยน

รูปที่ 1 การแสดงผลแบบขยาย กลาง และกะทัดรัดที่มีลิ้นชักการนำทาง แถบนำทาง และแถบด้านล่าง

การนำทาง UI ที่ตอบสนอง

ขนาดของหน้าต่างที่แสดงผลซึ่งแอปครอบครองจะส่งผลต่อลักษณะทางสรีรศาสตร์และความสามารถในการใช้งาน คลาสขนาดหน้าต่างช่วยให้คุณกำหนดองค์ประกอบการนําทางที่เหมาะสม (เช่น แถบนําทาง แถบแนวนอน หรือลิ้นชัก) และวางไว้ที่ผู้ใช้เข้าถึงได้ง่ายที่สุด ในหลักเกณฑ์ด้านเลย์เอาต์ของ 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>

ปลายทางของเนื้อหาที่ปรับเปลี่ยนตามอุปกรณ์

ใน UI ที่ปรับเปลี่ยนตามอุปกรณ์ เลย์เอาต์ของปลายทางเนื้อหาแต่ละรายการจะปรับตามการเปลี่ยนแปลงขนาดหน้าต่าง แอปสามารถปรับระยะห่างของเลย์เอาต์ เปลี่ยนตําแหน่งองค์ประกอบ เพิ่มหรือนําเนื้อหาออก หรือเปลี่ยนองค์ประกอบ UI รวมถึงองค์ประกอบการนําทาง

เมื่อปลายทางแต่ละแห่งจัดการเหตุการณ์การปรับขนาด การเปลี่ยนแปลงจะแยกอยู่ใน UI สถานะอื่นๆ ของแอป รวมถึงการนําทางจะไม่ได้รับผลกระทบ

การนำทางไม่ควรเกิดขึ้นจากผลข้างเคียงของการเปลี่ยนแปลงขนาดหน้าต่าง อย่าสร้างปลายทางของเนื้อหาเพื่อรองรับขนาดหน้าต่างที่แตกต่างกัน เช่น อย่าสร้างปลายทางเนื้อหาที่แตกต่างกันสำหรับหน้าจอต่างๆ ของอุปกรณ์แบบพับได้

การนำทางไปยังปลายทางของเนื้อหาซึ่งเป็นผลข้างเคียงของการเปลี่ยนแปลงขนาดหน้าต่างมีปัญหาต่อไปนี้

  • ปลายทางเดิม (สำหรับขนาดหน้าต่างก่อนหน้า) อาจปรากฏขึ้นชั่วครู่ก่อนที่จะไปยังปลายทางใหม่
  • ต้องมีการไปยังส่วนต่างๆ สำหรับขนาดหน้าต่างแต่ละขนาดเพื่อคงความสามารถในการเปลี่ยนกลับ (เช่น เมื่ออุปกรณ์พับขึ้นและพับลง)
  • การรักษาสถานะแอปพลิเคชันระหว่างปลายทางอาจทำได้ยาก เนื่องจากการนำทางอาจทำลายสถานะเมื่อแสดงสแต็กย้อนกลับ

นอกจากนี้ แอปอาจไม่ได้อยู่เบื้องหน้าเมื่อเกิดการเปลี่ยนแปลงขนาดหน้าต่าง เลย์เอาต์ของแอปอาจต้องใช้พื้นที่มากกว่าแอปที่ทำงานอยู่เบื้องหน้า และเมื่อผู้ใช้กลับมาที่แอปของคุณ การวางแนวและขนาดหน้าต่างอาจเปลี่ยนไป

หากแอปกำหนดให้ต้องมีปลายทางเนื้อหาที่ไม่ซ้ำกันตามขนาดหน้าต่าง ให้ลองรวมปลายทางที่เกี่ยวข้องไว้ในปลายทางเดียวที่มีเลย์เอาต์แบบปรับเปลี่ยน

ปลายทางของเนื้อหาที่มีเลย์เอาต์อื่น

ปลายทางการนำทางปลายทางเดียวอาจมีเลย์เอาต์อื่นโดยขึ้นอยู่กับขนาดหน้าต่างแอป ซึ่งเป็นส่วนหนึ่งของการออกแบบที่ปรับเปลี่ยนตามอุปกรณ์/แบบปรับเปลี่ยน เลย์เอาต์แต่ละรายการจะกินพื้นที่ทั้งหน้าต่าง แต่จะมีการแสดงเลย์เอาต์ที่แตกต่างกันสำหรับขนาดหน้าต่างที่แตกต่างกัน (การออกแบบที่ปรับเปลี่ยนได้)

ตัวอย่างที่ถือเป็น Canonical คือมุมมองรายการแบบละเอียด สำหรับขนาดหน้าต่างที่กะทัดรัด แอปจะแสดงเลย์เอาต์เนื้อหา 1 รายการสำหรับรายการและ 1 รายการสำหรับรายละเอียด การนำทางไปยังปลายทางของมุมมองรายละเอียดรายการจะแสดงเฉพาะเลย์เอาต์รายการในตอนแรก เมื่อเลือกรายการในรายการแล้ว แอปจะแสดงเลย์เอาต์แบบละเอียดแทนรายการ เมื่อเลือกตัวควบคุม "กลับ" เลย์เอาต์รายการจะแสดงแทนรายละเอียด อย่างไรก็ตาม สำหรับขนาดหน้าต่างแบบขยาย เลย์เอาต์รายการและรายละเอียดจะแสดงคู่กัน

SlidingPaneLayout ช่วยให้คุณสร้างปลายทางการนําทางเดียวได้ ซึ่งจะแสดงแผงเนื้อหา 2 แผงควบคู่กันในหน้าจอขนาดใหญ่ แต่แสดงได้เพียงแผงเดียวในแต่ละครั้งในหน้าจอขนาดเล็ก เช่น ในโทรศัพท์แบบดั้งเดิม

<!-- 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 ที่หัวข้อสร้างเลย์เอาต์แบบ 2 แผง

กราฟการนําทาง 1 รายการ

หากต้องการให้ผู้ใช้ได้รับประสบการณ์ที่สอดคล้องกันในอุปกรณ์หรือขนาดหน้าต่างใดก็ตาม ให้ใช้กราฟการนําทางเดียวที่เลย์เอาต์ของปลายทางเนื้อหาแต่ละรายการเป็นแบบตอบสนอง

หากคุณใช้กราฟการนําทางที่แตกต่างกันสําหรับคลาสขนาดหน้าต่างแต่ละคลาส เมื่อแอปเปลี่ยนจากคลาสขนาดหนึ่งไปยังอีกคลาสหนึ่ง คุณต้องระบุปลายทางปัจจุบันของผู้ใช้ในกราฟอื่น สร้างกองซ้อนที่ย้อนกลับ และปรับยอดข้อมูลสถานะที่ต่างกันระหว่างกราฟ

โฮสต์การนําทางที่ฝัง

แอปของคุณอาจมีปลายทางเนื้อหาที่มีปลายทางเนื้อหาเป็นของตัวเอง เช่น ในเลย์เอาต์รายการแบบละเอียด แผงรายละเอียดรายการอาจประกอบด้วยองค์ประกอบ UI ที่ไปยังเนื้อหาที่จะแทนที่รายละเอียดรายการ

หากต้องการใช้การนําทางย่อยประเภทนี้ ให้ทําให้แผงรายละเอียดเป็นโฮสต์การนําทางที่ฝังอยู่ซึ่งมีกราฟการนําทางของตัวเองซึ่งระบุปลายทางที่เข้าถึงได้จากแผงรายละเอียด

<!-- 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 ที่ฝังอยู่ไม่ได้เชื่อมต่อกับกราฟการนำทางหลัก กล่าวคือ คุณไม่สามารถไปยังปลายทางในกราฟหนึ่งไปยังปลายทางในอีกกราฟหนึ่งได้โดยตรง

ดูข้อมูลเพิ่มเติมได้ที่กราฟการนําทางที่ฝัง

สถานะที่สงวนไว้

หากต้องการแสดงปลายทางของเนื้อหาที่ปรับเปลี่ยนได้ แอปของคุณต้องรักษาสถานะไว้เมื่อมีการพลิกหรือพับอุปกรณ์ หรือปรับขนาดหน้าต่างแอป โดยค่าเริ่มต้น การเปลี่ยนแปลงการกําหนดค่าเช่นนี้จะสร้างกิจกรรม ข้อมูลโค้ด และลําดับชั้นมุมมองของแอปขึ้นมาใหม่ วิธีที่เราแนะนําในการบันทึกสถานะ UI คือใช้ ViewModel ซึ่งจะยังคงอยู่แม้มีการเปลี่ยนแปลงการกําหนดค่า (ดูบันทึกสถานะ UI )

การเปลี่ยนแปลงขนาดควรเปลี่ยนกลับได้ เช่น เมื่อผู้ใช้หมุนอุปกรณ์แล้วหมุนกลับ

เลย์เอาต์ที่ปรับเปลี่ยนตามอุปกรณ์/ที่ปรับเปลี่ยนได้สามารถแสดงเนื้อหาที่แตกต่างกันในหน้าต่างขนาดต่างๆ ดังนั้นเลย์เอาต์ที่ปรับเปลี่ยนตามอุปกรณ์จึงมักต้องบันทึกสถานะเพิ่มเติมที่เกี่ยวข้องกับเนื้อหา แม้ว่าสถานะนั้นจะใช้ไม่ได้กับขนาดหน้าต่างปัจจุบัน เช่น เลย์เอาต์อาจมีพื้นที่แสดงวิดเจ็ตการเลื่อนเพิ่มเติมก็ต่อเมื่อหน้าต่างมีขนาดใหญ่ขึ้นเท่านั้น หากเหตุการณ์การปรับขนาดทําให้ความกว้างของหน้าต่างแคบเกินไป วิดเจ็ตจะซ่อนอยู่ เมื่อแอปปรับขนาดกลับไปเป็นขนาดก่อนหน้า วิดเจ็ตที่เลื่อนจะปรากฏขึ้นอีกครั้ง และตำแหน่งการเลื่อนเดิมควรได้รับการคืนค่า

ขอบเขต ViewModel

คู่มือนักพัฒนาซอฟต์แวร์ย้ายข้อมูลไปยังคอมโพเนนต์การนําทางกําหนดสถาปัตยกรรมกิจกรรมเดียวซึ่งมีการใช้ปลายทางเป็นข้อมูลโค้ดที่แยกส่วน และติดตั้งใช้งานโมเดลข้อมูลโดยใช้ ViewModel

ViewModel จะกำหนดขอบเขตตามวงจรการทำงานเสมอ และเมื่อวงจรการทำงานนั้นสิ้นสุดลงอย่างถาวร ระบบจะล้าง ViewModel และทิ้งได้ วงจรชีวิตของ ViewModel ซึ่งกำหนดขอบเขตการแชร์ ViewModel นั้นขึ้นอยู่กับตัวรับสิทธิ์พร็อพเพอร์ตี้ที่ใช้รับ ViewModel

ในกรณีที่ง่ายที่สุด ปลายทางการนำทางทุกแห่งจะเป็นส่วนที่แยกต่างหากซึ่งมีสถานะ UI ที่แยกจากกันโดยสิ้นเชิง ดังนั้นแต่ละส่วนจึงสามารถใช้ตัวรับช่วงพร็อพเพอร์ตี้ viewModels() เพื่อรับ ViewModel ที่มีขอบเขตระดับส่วนที่แยกต่างหากนั้น

หากต้องการแชร์สถานะ UI ระหว่างข้อมูลโค้ด ให้กําหนดขอบเขต ViewModel ให้กับกิจกรรมโดยเรียกใช้ activityViewModels() ในข้อมูลโค้ด (เทียบเท่าสําหรับ Activity คือ viewModels()) ซึ่งจะช่วยให้กิจกรรมและข้อมูลโค้ดที่แนบมาแชร์อินสแตนซ์ ViewModel ได้ อย่างไรก็ตาม ในสถาปัตยกรรมแบบกิจกรรมเดียว ขอบเขต ViewModel นี้จะใช้งานได้ตราบใดที่แอปยังทำงานอยู่ ดังนั้น ViewModel จะยังคงอยู่ในหน่วยความจำแม้ว่าจะไม่มีการใช้งานจากข้อมูลโค้ดที่แยกส่วนก็ตาม

สมมติว่ากราฟการนําทางมีลําดับปลายทางของข้อมูลโค้ดที่แสดงขั้นตอนการชำระเงิน และสถานะปัจจุบันของประสบการณ์การชําระเงินทั้งหมดอยู่ใน ViewModel ที่แชร์ระหว่างข้อมูลโค้ด การกําหนดขอบเขต ViewModel ให้กับกิจกรรมไม่เพียงแต่จะกว้างเกินไป แต่ยังทำให้เกิดปัญหาอีกอย่างหนึ่งด้วย กล่าวคือ หากผู้ใช้ทําตามขั้นตอนการชำระเงินสําหรับคําสั่งซื้อ 1 รายการ แล้วทําตามขั้นตอนดังกล่าวอีกครั้งสําหรับคําสั่งซื้อที่ 2 คําสั่งซื้อทั้ง 2 รายการจะใช้อินสแตนซ์เดียวกันของ ViewModel การชำระเงิน ก่อนชำระเงินสำหรับคำสั่งซื้อที่ 2 คุณต้องล้างข้อมูลออกจากคำสั่งซื้อแรกด้วยตนเอง ความผิดพลาดใดๆ อาจทำให้ผู้ใช้เสียค่าใช้จ่าย

แต่ให้กําหนดขอบเขต ViewModel ให้กับกราฟการนําทางใน NavController ปัจจุบันแทน สร้างกราฟการนําทางที่ซ้อนกันเพื่อรวมปลายทางที่เป็นส่วนหนึ่งของขั้นตอนการชำระเงิน จากนั้นในแต่ละปลายทางของข้อมูลโค้ดที่ฝัง ให้ใช้ตัวรับค่าพร็อพเพอร์ตี้ navGraphViewModels() และส่งรหัสของกราฟการนำทางเพื่อรับ ViewModel ที่แชร์ วิธีนี้ช่วยให้มั่นใจว่าเมื่อผู้ใช้ออกจากขั้นตอนการชำระเงินและกราฟการนําทางที่ฝังอยู่อยู่นอกขอบเขต ระบบจะทิ้งอินสแตนซ์ของ ViewModel ที่เกี่ยวข้องและจะไม่นําไปใช้สําหรับการชําระเงินครั้งถัดไป

ขอบเขต ผู้รับมอบสิทธิ์พร็อพเพอร์ตี้ แชร์ ViewModel กับ
ส่วนย่อย Fragment.viewModels() เฉพาะข้อมูลโค้ด
กิจกรรม Activity.viewModels() หรือ Fragment.activityViewModels() กิจกรรมและเศษข้อมูลทั้งหมดที่แนบมา
กราฟการนําทาง Fragment.navGraphViewModels() ข้อมูลโค้ดที่ฝังทั้งหมดในกราฟการนําทางเดียวกัน

โปรดทราบว่าหากคุณใช้โฮสต์การนำทางที่ฝัง (ดูส่วนโฮสต์การนำทางที่ฝัง) ปลายทางในโฮสต์นั้นจะแชร์อินสแตนซ์ ViewModel กับปลายทางภายนอกโฮสต์ไม่ได้เมื่อใช้ navGraphViewModels() เนื่องจากกราฟไม่ได้เชื่อมต่อกัน ในกรณีนี้ คุณจะใช้ขอบเขตกิจกรรมแทนได้

แหล่งข้อมูลเพิ่มเติม