반응형 탐색 빌드

탐색은 애플리케이션의 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에만 적용됩니다. 탐색을 포함한 앱 상태의 나머지 부분은 영향을 받지 않습니다.

창 크기 변경의 부수 효과로 탐색이 발생하면 안 됩니다. 다양한 창 크기를 반영할 목적으로 콘텐츠 대상을 만들지는 마세요. 예를 들어 폴더블 기기의 여러 화면에 맞도록 콘텐츠 대상을 여러 개 만들어서는 안 됩니다.

창 크기 변경의 부수 효과로 콘텐츠 대상으로 탐색하면 다음과 같은 문제가 발생합니다.

  • 새 대상으로 이동하기 전에 이전 대상 (이전 창 크기)이 잠시 표시될 수 있습니다.
  • (예를 들어 기기를 접었다가 펴는 상황에서) 가역성을 유지하기 위해 창 크기마다 탐색이 필요합니다.
  • 탐색 시 백 스택이 표시될 때 상태가 소멸될 수 있기 때문에 대상이 바뀔 때 애플리케이션 상태를 유지하기가 어려울 수 있습니다.

또한 창 크기가 변경되는 동안 앱이 포그라운드에 있지 않을 수도 있습니다. 앱의 레이아웃에는 포그라운드 앱보다 공간이 더 많이 필요할 수 있으며 사용자가 앱으로 돌아올 때 방향과 창 크기가 모두 변경되었을 수 있습니다.

앱에 창 크기에 기반한 고유한 콘텐츠 대상이 필요한 경우 관련 대상을 대체 적응형 레이아웃이 포함된 단일 대상으로 결합하는 것이 좋습니다.

대체 레이아웃이 있는 콘텐츠 대상

반응형/적응형 디자인의 일환으로 단일 탐색 대상은 앱 창 크기에 따라 대체 레이아웃을 보유할 수 있습니다. 각 레이아웃은 전체 창을 차지하지만 다양한 레이아웃이 여러 창 크기에 맞게 표시됩니다(적응형 디자인).

표준적인 예로는 목록 세부정보 뷰가 있습니다. 좁은 창 크기의 경우 앱은 목록에 대한 콘텐츠 레이아웃을 하나 표시하고 세부정보에 대해 하나를 표시합니다. 목록 세부정보 뷰 대상으로 이동하면 처음에는 목록 레이아웃만 표시됩니다. 목록 항목을 선택하면 앱에서 세부정보 레이아웃을 표시하여 목록을 대체합니다. 뒤로 컨트롤을 선택하면 목록 레이아웃이 표시되어 세부정보를 대체합니다. 그러나 확장된 창 크기에서는 목록 레이아웃과 세부정보 레이아웃이 나란히 표시됩니다.

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을 사용하여 목록 세부정보 레이아웃을 구현하는 방법에 관한 자세한 내용은 두 개의 창 레이아웃 만들기를 참고하세요.

단일 탐색 그래프

모든 기기 또는 창 크기에서 일관된 사용자 환경을 제공하려면 각 콘텐츠 대상의 레이아웃이 반응형인 단일 탐색 그래프를 사용하세요.

창 크기 클래스별로 다른 탐색 그래프를 사용한다면 앱이 특정 크기 클래스에서 다른 크기 클래스로 전환될 때마다 다른 그래프에서 사용자의 현재 대상을 확인하고 백 스택을 구성하고 그래프 간에 다른 상태 정보를 조정해야 합니다.

중첩된 탐색 호스트

앱은 자체 콘텐츠 대상이 있는 콘텐츠 대상을 포함할 수 있습니다. 예를 들어 목록 세부정보 레이아웃에서 항목 세부정보 창에는 항목 세부정보를 대체하는 콘텐츠로 이동하는 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 상태를 공유하려면 프래그먼트에서 activityViewModels()를 호출하여 ViewModel의 범위를 활동으로 지정합니다 (Activity에 상응하는 것은 viewModels()). 이렇게 하면 활동과 활동에 연결되는 프래그먼트가 ViewModel 인스턴스를 공유할 수 있습니다. 그러나 단일 활동 아키텍처에서는 이 ViewModel 범위가 앱이 지속되는 한 사실상 지속되므로 프래그먼트에서 ViewModel을 사용하지 않더라도 ViewModel이 메모리에 유지됩니다.

탐색 그래프에 결제 흐름을 나타내는 프래그먼트 대상 시퀀스가 있고 전체 결제 환경의 현재 상태가 프래그먼트 간에 공유되는 ViewModel이라고 가정해 보겠습니다. ViewModel의 범위를 활동으로 지정하는 것은 너무 광범위할 뿐 아니라 실제로 또 다른 문제를 노출합니다. 사용자가 특정 주문의 결제 흐름을 진행한 후 두 번째 주문의 결제 흐름을 다시 진행하면 두 주문에서 모두 같은 결제 ViewModel 인스턴스를 사용합니다. 두 번째 주문 결제 전에 첫 번째 주문의 데이터를 수동으로 지워야 합니다. 실수하면 사용자에게 큰 비용이 발생할 수 있습니다.

대신 현재 NavController의 탐색 그래프로 ViewModel의 범위를 지정합니다. 중첩된 탐색 그래프를 만들어 결제 흐름의 일부인 대상을 캡슐화합니다. 그런 다음 이러한 각 프래그먼트 대상에서 navGraphViewModels() 속성 위임을 사용하고 탐색 그래프의 ID를 전달하여 공유된 ViewModel을 가져옵니다. 이렇게 하면 사용자가 결제 흐름을 종료하고 중첩된 탐색 그래프가 범위를 벗어나면 상응하는 ViewModel 인스턴스가 삭제되고 다음 결제에 사용되지 않습니다.

범위 속성 위임 ViewModel 공유 대상
Fragment Fragment.viewModels() 프래그먼트만
활동 Activity.viewModels() 또는 Fragment.activityViewModels() 활동과 활동에 연결된 모든 프래그먼트
탐색 그래프 Fragment.navGraphViewModels() 같은 탐색 그래프의 모든 프래그먼트

중첩된 탐색 호스트 (중첩된 탐색 호스트 섹션 참고)를 사용하고 있다면 navGraphViewModels()를 사용할 때 이 호스트의 대상이 호스트 외부의 대상과 ViewModel 인스턴스를 공유할 수 없습니다. 그래프가 연결되어 있지 않기 때문입니다. 이 경우 활동 범위를 대신 사용할 수 있습니다.

추가 리소스