탐색은 애플리케이션의 UI와 상호작용하여 콘텐츠 대상에 액세스하는 사용자 작업입니다. Android의 탐색 원리는 일관되고 직관적인 앱 탐색을 만드는 데 도움이 되는 가이드라인을 제공합니다.
반응형/적응형 UI는 반응형 콘텐츠 대상을 제공하며, 화면 크기 변경에 따라 다양한 유형의 탐색 요소를 포함하는 경우가 많습니다(예: 소형 디스플레이의 하단 탐색 메뉴, 중형 디스플레이의 탐색 레일, 대형 디스플레이의 영구 탐색 창). 하지만 반응형/적응형 UI는 탐색 원칙을 준수해야 합니다.
Jetpack 탐색 구성요소는 탐색 원리를 구현하며 반응형/적응형 UI로 앱 개발을 용이하게 합니다.
반응형 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
인스턴스를 공유할 수 없습니다. 그래프가 연결되어 있지 않기 때문입니다. 이 경우 활동 범위를 대신 사용할 수 있습니다.