안드로이드 앱 내비게이션은 3개로 구분된다. 아래 순서대로 내비게이션 방식을 차례로 살펴본다.
추가로 primary(주요) 목적지와 secondary(보조) 목적지 간 차이점을 설명하고, 앱의 유즈케이스 별로 어떤 내비게이션을 선택해야 하는지 설명한다.
여기서 다룰 내용은 다음과 같다.
주요 목적지란 앱 내에서 항상 표시되는 목적지를 얘기한다. 보조 목적지는 주요 목적지 아래에 위치하며, 필요 시 접근 가능하다.
사용자는 자신이 어디에 있는지 앱 바를 통해 알 수 있다.
내비게이션 드로어는 안드로이드에서 가장 오래된 내비게이션 패턴이며, 닫혀있을 땐 앱 바에 햄버거 메뉴를 보여준다. 햄버거 메뉴를 탭 하면 왼쪽으로 메뉴 리스트가 보이게 된다. 네비게이션 드로어는 여러 목적지에 빠르게 접근할 때 유용하다.
단점은 햄버거 메뉴를 꼭 선택해야 한다는 것이다. 이에 반해 바텀, 탭 내비게이션 패턴은 화면 내 목적지들이 보인다. 하지만 반대로 내비게이션 드로어는 화면을 더 여유롭게 사용할 수 있다는 말이기도 하니 장점도 있다..
내가 보기엔 진짜 단점은 설정하기가 무지하게 복잡하고 내용이 많다는 것이다. 다 의미가 있는 행동이겠지만 더 쉽게 만들 수는 없었을까?
android:theme 를 아래와 같이 설정한다.<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.NavigationDrawer.NoActionBar">
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mobile_navigation.xml"
app:startDestination="@id/nav_home">
<fragment
android:id="@+id/nav_home"
android:name="com.example.navigationdrawer.HomeFragment"
android:label="@string/home"
tools:layout="@layout/fragment_home">
<action
android:id="@+id/nav_home_to_content"
app:destination="@id/nav_content"
app:popUpTo="@id/nav_home" />
</fragment>
<fragment
android:id="@+id/nav_content"
android:name="com.example.navigationdrawer.ContentFragment"
android:label="@string/content"
tools:layout="@layout/fragment_content" />
<!-- 아래 더 많음 -->
</navigation>
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_home, container, false)
view.findViewById<Button>(R.id.button_home)?.setOnClickListener(
Navigation.createNavigateOnClickListener(R.id.nav_home_to_content, null))
return view
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation"/>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="176dp"
android:background="@color/teal_700"
android:gravity="bottom"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/nav_header_desc"
android:paddingTop="8dp"
app:srcCompat="@mipmap/ic_launcher_round" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- MARK - App bar -->
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Theme.NavigationDrawer.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/Theme.NavigationDrawer.PopupOverlay"/>
</com.google.android.material.appbar.AppBarLayout>
<!-- MARK - Contents -->
<include layout="@layout/content_main" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:showIn="navigation_view">
<group
android:id="@+id/menu_top"
android:checkableBehavior="single">
<item
android:id="@+id/nav_home"
android:icon="@drawable/home"
android:title="@string/home" />
<item
android:id="@+id/nav_recent"
android:icon="@drawable/recent"
android:title="@string/recent" />
<item
android:id="@+id/nav_favorites"
android:icon="@drawable/favorites"
android:title="@string/favorites" />
</group>
<group
android:id="@+id/menu_bottom"
android:checkableBehavior="single" >
<item
android:id="@+id/nav_archive"
android:icon="@drawable/archive"
android:title="@string/archive" />
<item
android:id="@+id/nav_bin"
android:icon="@drawable/bin"
android:title="@string/bin" />
</group>
</menu>
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/nav_settings"
android:title="@string/settings"
app:showAsAction="never" />
</menu>
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="start">
<include
layout="@layout/app_bar_main"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- app:headerLayout 에는 내가 만든 헤더 -->
<!-- app:menu 에는 내가 만든 메뉴 목록 -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header_main"
app:menu="@menu/activity_main_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Toolbar 세팅
setSupportActionBar(findViewById(R.id.toolbar))
// NavHostFragment 가져옴
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
// NavHostFragment 를 통해 NavController 를 가져옴
val navController = navHostFragment.navController
// Navigation Drawer 세팅 (여기서 설정하는 메뉴들은 primary destination)
// 즉, setOf 안의 Destination ID 들은 다른 Destination ID 들 중 앱 바에 햄버거 메뉴를 노출해야 할 ID 인 것이다.
// setOf 다음 parameter 인 drawer_layout 은 햄버거 메뉴를 선택 했을 때 보여줘야 할 레이아웃이다.
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.nav_home, R.id.nav_recent, R.id.nav_favorites,
R.id.nav_archive, R.id.nav_bin
), findViewById(R.id.drawer_layout)
)
// 앱 바와 내비게이션 그래프 연결
setupActionBarWithNavController(navController, appBarConfiguration)
// 내비게이션 드로어의 한 항목을 클릭했을 떄 강조 표시해야 하는 항목을 지정
findViewById<NavigationView>(R.id.nav_view)
?.setupWithNavController(navController)
}
// 뒤로가기 버튼 클릭 시 부모 목적지 돌아가는 작업 처리
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
// 앱 바에 추가할 메뉴를 결정
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main, menu)
return true
}
// 드로어 메뉴 선택 시
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(
findNavController(R.id.nav_host_fragment))
}
}
최상위 목적지 (primary destination) 이 적은 경우 유용하다. 앱의 보조 목적지 내에서 빠르게 항상 이용 가능한 내비게이션을 목적으로 사용된다.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize">
<!-- 새로운 바텀 내비게이션 뷰 --> <com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/nav_view"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="0dp"
android:layout_marginEnd="0dp"
android:background="?android:attr/windowBackground"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:menu="@menu/bottom_nav_menu"
app:labelVisibilityMode="labeled" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:defaultNavHost="true"
app:navGraph="@navigation/mobile_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
appBarConfiguration = AppBarConfiguration(
setOf(R.id.nav_home, R.id.nav_tickets, R.id.nav_offers, R.id.nav_rewards))
setupActionBarWithNavController(navController,
appBarConfiguration)
findViewById<BottomNavigationView>(R.id.nav_view)
?.setupWithNavController(navController)
}
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.
navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.main, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
super.onOptionsItemSelected(item)
return item.onNavDestinationSelected(findNavController(
R.id.nav_host_fragment
))
}
}
관련 항목을 표시할 때 유용하다. 2~5개 사이 탭을 표시하고 그 이상의 탭은 스크롤로 처리한다.
주로 바텀 내비게이션과 함께 사용한다.