Xây dựng thành phần điều hướng thích ứng

Điều hướng là hoạt động tương tác của người dùng với giao diện người dùng của ứng dụng để truy cập vào đích đến nội dung. Nguyên tắc điều hướng của Android giúp bạn xây dựng việc điều hướng trong ứng dụng một cách nhất quán và trực quan.

Giao diện người dùng thích ứng/đáp ứng cung cấp các đích đến nội dung thích ứng và thường bao gồm nhiều loại thành phần điều hướng để phản hồi các thay đổi về kích thước màn hình, ví dụ: thanh điều hướng ở dưới cùng trên màn hình nhỏ, dải điều hướng trên màn hình cỡ trung bình hoặc ngăn điều hướng cố định trên màn hình lớn. Tuy nhiên, giao diện người dùng thích ứng/đáp ứng vẫn phải tuân thủ các nguyên tắc điều hướng.

Thành phần điều hướng Jetpack triển khai các nguyên tắc điều hướng và hỗ trợ phát triển các ứng dụng có giao diện người dùng thích ứng/đáp ứng.

Hình 1. Màn hình mở rộng, trung bình và nhỏ gọn có ngăn điều hướng, dải điều hướng và thanh dưới cùng màn hình.

Điều hướng giao diện người dùng thích ứng

Kích thước của cửa sổ hiển thị mà một ứng dụng hiện diện ảnh hưởng đến tính tiện dụng và khả năng hữu dụng. Các lớp kích thước cửa sổ cho phép bạn xác định các phần tử điều hướng thích hợp (chẳng hạn như thanh điều hướng, ray hoặc ngăn) và đặt các phần tử đó ở nơi người dùng có thể truy cập dễ dàng nhất. Trong nguyên tắc về bố cục của Material Design, các phần tử điều hướng chiếm một không gian ổn định ở cạnh trên của màn hình và có thể di chuyển sang cạnh dưới cùng khi chiều rộng của ứng dụng nhỏ gọn. Lựa chọn phần tử điều hướng phụ thuộc phần lớn vào kích thước của cửa sổ ứng dụng và số mục mà phần tử phải chứa.

Lớp kích thước cửa sổ Một vài mục Nhiều mục
chiều rộng thu gọn thanh điều hướng ở dưới cùng ngăn điều hướng (cạnh trên hoặc cuối)
chiều rộng trung bình Dải điều hướng ngăn điều hướng (cạnh trên)
chiều rộng được mở rộng Dải điều hướng ngăn điều hướng cố định (cạnh trên)

Tệp tài nguyên bố cục có thể đủ điều kiện theo điểm ngắt lớp kích thước cửa sổ để sử dụng các phần tử điều hướng khác nhau cho các phương diện màn hình khác nhau.

<!-- 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>

Đích đến của nội dung thích ứng

Trong giao diện người dùng thích ứng, bố cục của từng đích đến nội dung sẽ thích ứng với sự thay đổi về kích thước cửa sổ. Ứng dụng của bạn có thể điều chỉnh khoảng cách bố cục, phần tử vị trí, thêm/xoá nội dung hoặc thay đổi các phần tử giao diện người dùng, bao gồm cả các phần tử điều hướng.

Khi mỗi đích đến riêng lẻ xử lý các sự kiện đổi kích thước, các thay đổi sẽ được tách riêng khỏi giao diện người dùng. Phần còn lại của trạng thái ứng dụng bao gồm cả trạng thái điều hướng sẽ không bị ảnh hưởng.

Hoạt động điều hướng không nên xuất hiện như tác dụng phụ của việc thay đổi kích thước cửa sổ. Không tạo đích đến nội dung chỉ để phù hợp với kích thước cửa sổ khác nhau. Ví dụ: không tạo các đích đến khác nhau cho nội dung cho các màn hình khác nhau của thiết bị có thể gập.

Việc điều hướng đến đích đến nội dung như tác dụng phụ của việc thay đổi kích thước cửa sổ sẽ có các vấn đề sau:

  • Đích đến cũ (đối với kích thước cửa sổ trước đó) có thể hiển thị tạm thời trước khi điều hướng đến đích đến mới
  • Để duy trì khả năng đảo ngược (ví dụ: khi thiết bị gập và mở) thì phải buộc điều hướng cho mỗi kích thước cửa sổ
  • Việc duy trì trạng thái ứng dụng giữa các đích đến có thể khó khăn do việc điều hướng có thể phá huỷ trạng thái khi bật ngăn xếp lui

Ngoài ra, ứng dụng của bạn thậm chí có thể không chạy ở nền trước khi các thay đổi về kích thước cửa sổ diễn ra. Bố cục của ứng dụng có thể yêu cầu nhiều không gian hơn ứng dụng trên nền trước. Và khi người dùng quay lại ứng dụng của bạn, hướng và kích thước cửa sổ đều có thể đã thay đổi.

Nếu ứng dụng của bạn yêu cầu đích đến nội dung duy nhất dựa trên kích thước cửa sổ, hãy cân nhắc kết hợp các đích đến liên quan thành một đích đến duy nhất bao gồm các bố cục thay thế, thích ứng.

Đích đến của nội dung có bố cục thay thế

Là một phần của thiết kế thích ứng/đáp ứng, một đích đến điều hướng duy nhất có thể có các bố cục thay thế tuỳ thuộc vào kích thước cửa sổ ứng dụng. Bố cục nào cũng chiếm toàn bộ cửa sổ, nhưng mỗi bố cục lại xuất hiện cho từng kích thước cửa sổ riêng (thiết kế thích ứng).

Một ví dụ chuẩn là thành phần hiển thị danh sách-chi tiết (list-detail). Đối với các kích thước cửa sổ nhỏ gọn, ứng dụng sẽ hiện một bố cục nội dung cho danh sách và một bố cục cho thông tin chi tiết. Ban đầu, việc điều hướng tới đích đến của thành phần hiển thị danh sách-chi tiết chỉ cho thấy bố cục danh sách. Khi một mục trong danh sách được chọn, ứng dụng sẽ hiện bố cục chi tiết thay cho danh sách. Khi bạn chọn nút điều khiển quay lại, bố cục danh sách sẽ hiển thị thay thế cho thông tin chi tiết. Tuy nhiên, đối với kích thước cửa sổ mở rộng, bố cục danh sách và chi tiết sẽ xuất hiện bên cạnh nhau.

SlidingPaneLayout cho phép bạn tạo một đích đến điều hướng duy nhất cho thấy hai ngăn nội dung cạnh nhau trên màn hình lớn, nhưng chỉ cho thấy một ngăn trên màn hình nhỏ chẳng hạn như trên điện thoại thông thường.

<!-- 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>

Hãy xem phần Tạo bố cục hai ngăn để nắm được chi tiết về cách triển khai bố cục danh sách-chi tiết bằng SlidingPaneLayout.

Biểu đồ điều hướng

Để mang đến trải nghiệm nhất quán cho người dùng trên mọi kích thước thiết bị hoặc cửa sổ, hãy sử dụng một biểu đồ điều hướng duy nhất, trong đó bố cục của mỗi đích đến nội dung đều có tính thích ứng.

Nếu sử dụng biểu đồ điều hướng riêng cho từng lớp kích thước cửa sổ, mỗi khi ứng dụng chuyển đổi từ một lớp kích thước này sang lớp kích thước khác, thì bạn phải xác định đích đến hiện tại của người dùng trong các sơ đồ khác, tạo ngăn xếp lui và điều chỉnh thông tin trạng thái khác biệt giữa các sơ đồ.

Lưu trữ điều hướng lồng

Ứng dụng của bạn có thể bao gồm một đích đến nội dung có các đích đến nội dung riêng. Ví dụ: trong bố cục danh sách-chi tiết, ngăn chi tiết mục có thể bao gồm các thành phần trên giao diện người dùng điều hướng đến nội dung thay thế cho chi tiết của mục.

Để triển khai loại hình điều hướng phụ này, hãy đặt ngăn chi tiết làm một lưu trữ điều hướng được lồng với biểu đồ điều hướng riêng và chỉ định các đích đến được truy cập qua ngăn chi tiết:

<!-- 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>

Biểu đồ này khác với biểu đồ điều hướng lồng nhau vì biểu đồ điều hướng của NavHost lồng nhau không kết nối với biểu đồ điều hướng chính; nghĩa là bạn không thể chuyển trực tiếp từ các đích đến trong một biểu đồ sang các đích đến ở một biểu đồ khác.

Để biết thêm thông tin, hãy xem phần Biểu đồ điều hướng lồng nhau.

Trạng thái lưu giữ

Để cung cấp đích đến nội dung có tính thích ứng, ứng dụng của bạn phải duy trì trạng thái khi xoay hoặc gập thiết bị hoặc đổi kích thước cửa sổ ứng dụng. Theo mặc định, các thay đổi về cấu hình, chẳng hạn như thay đổi về hoạt động, phân mảnh và phân cấp thành phần hiển thị của ứng dụng. Bạn nên lưu trạng thái giao diện người dùng bằng cách sử dụng ViewModel. Trạng thái này vẫn tồn tại khi bạn thay đổi cấu hình. (Xem phần Lưu trạng thái giao diện người dùng .)

Các thay đổi về kích thước phải đảo ngược được – ví dụ: khi người dùng xoay thiết bị, sau đó xoay trở lại.

Bố cục thích ứng có thể cho thấy những phần nội dung riêng biệt tuỳ theo kích thước cửa sổ, và do đó bố cục thích ứng thường cần lưu trạng thái bổ sung liên quan đến nội dung, ngay cả khi trạng thái đó không áp dụng cho kích thước cửa sổ hiện tại. Ví dụ: có thể một bố cục có không gian trình bày một tiện ích cuộn bổ sung chỉ ở chiều rộng cửa sổ lớn. Nếu sự kiện đổi kích thước khiến chiều rộng của cửa sổ trở nên quá nhỏ, thì tiện ích sẽ bị ẩn. Khi ứng dụng trở lại kích thước trước đó, tiện ích cuộn này sẽ xuất hiện lại và vị trí cuộn ban đầu sẽ được khôi phục.

Phạm vi ViewModel

Hướng dẫn dành cho nhà phát triển Di chuyển sang thành phần Điều hướng quy định một cấu trúc hoạt động đơn trong đó các đích đến được triển khai dưới dạng các phân mảnh và mô hình dữ liệu của chúng được triển khai sử dụng ViewModel.

ViewModel luôn thuộc phạm vi một vòng đời và khi vòng đời đó kết thúc vĩnh viễn, ViewModel sẽ bị xoá và có thể bị loại bỏ. Vòng đời chứa ViewModel và do đó ViewModel có thể được chia sẻ rộng rãi phụ thuộc vào việc uỷ quyền thuộc tính nào dùng để lấy ViewModel.

Trong trường hợp đơn giản nhất, mỗi đích đến điều hướng đều là một phân mảnh duy nhất có trạng thái giao diện người dùng hoàn toàn tách biệt; và do đó, mỗi phân mảnh có thể sử dụng uỷ quyền thuộc tính viewModels() để có được ViewModel xác định phạm vi theo phân mảnh đó.

Để chia sẻ trạng thái giao diện người dùng giữa các mảnh, hãy đặt phạm vi ViewModel cho hoạt động bằng cách gọi activityViewModels() trong các mảnh (giá trị tương đương với Activity chỉ là viewModels()). Việc này cho phép hoạt động và mọi mảnh đính kèm với hoạt động này chia sẻ phiên bản ViewModel. Tuy nhiên, trong một cấu trúc hoạt động đơn, phạm vi ViewModel này cũng tồn tại lâu như ứng dụng, cũng giống như ViewModel tồn tại trong bộ nhớ ngay cả khi không có mảnh nào đang sử dụng nó.

Giả sử biểu đồ điều hướng của bạn có một chuỗi các đích đến phân mảnh đại diện cho một luồng thanh toán và trạng thái hiện tại cho toàn bộ trải nghiệm thanh toán nằm trong một ViewModel được chia sẻ giữa các phân mảnh. Việc đưa ViewModel vào hoạt động không chỉ quá rộng mà còn thực sự làm phát sinh một vấn đề khác: nếu người dùng trải qua quy trình thanh toán cho một đơn đặt hàng và sau đó thực hiện lại đơn đặt hàng thứ hai, thì cả hai đơn đặt hàng đều sử dụng cùng một phiên bản thanh toán ViewModel. Trước khi thanh toán đơn đặt hàng thứ hai, bạn phải tự xoá dữ liệu của đơn đặt hàng đầu tiên. Mọi lỗi đều có thể gây tổn thất cho người dùng.

Thay vào đó, hãy đặt phạm vi ViewModel vào một biểu đồ điều hướng trong NavController hiện tại. Tạo một biểu đồ điều hướng lồng nhau để đóng gói các đích đến thuộc quy trình thanh toán. Sau đó, trong mỗi đích đến phân mảnh đó, hãy sử dụng uỷ quyền thuộc tính navGraphViewModels() và chuyển mã của biểu đồ điều hướng để lấy ViewModel được chia sẻ. Điều này đảm bảo rằng khi người dùng thoát khỏi quy trình thanh toán và biểu đồ điều hướng được lồng nằm ngoài phạm vi, phiên bản tương ứng của ViewModel sẽ được loại bỏ và không được dùng cho lần thanh toán tiếp theo.

Phạm vi Ủỷ quyền thuộc tính Có thể chia sẻ ViewModel với
Mảnh Fragment.viewModels() Chỉ mảnh
Hoạt động Activity.viewModels() hoặc Fragment.activityViewModels() Hoạt động và tất cả các phân mảnh đính kèm với hoạt động đó
Biểu đồ điều hướng Fragment.navGraphViewModels() Tất cả các phân mảnh trong cùng một biểu đồ điều hướng

Lưu ý nếu bạn đang sử dụng máy chủ điều hướng lồng nhau (xem phần Máy chủ điều hướng lồng nhau), thì các đích đến trong máy chủ đó không thể chia sẻ các thực thể ViewModel với các đích đến bên ngoài máy chủ khi sử dụng navGraphViewModels() vì các biểu đồ chưa được kết nối. Trong trường hợp này, bạn có thể sử dụng phạm vi hoạt động.

Tài nguyên khác