Android 공부 (4)

백상휘·2025년 10월 10일
0

Android

목록 보기
2/5

안드로이드 앱 내비게이션은 3개로 구분된다. 아래 순서대로 내비게이션 방식을 차례로 살펴본다.

  1. Drawer
  2. Bottom
  3. Tabbed

추가로 primary(주요) 목적지와 secondary(보조) 목적지 간 차이점을 설명하고, 앱의 유즈케이스 별로 어떤 내비게이션을 선택해야 하는지 설명한다.

여기서 다룰 내용은 다음과 같다.

  • 내비게이션 개요
  • 내비게이션 드로어
  • 바텀 내비게이션
  • 탭 내비게이션

내비게이션 개요

주요 목적지란 앱 내에서 항상 표시되는 목적지를 얘기한다. 보조 목적지는 주요 목적지 아래에 위치하며, 필요 시 접근 가능하다.

사용자는 자신이 어디에 있는지 앱 바를 통해 알 수 있다.

내비게이션 드로어

내비게이션 드로어는 안드로이드에서 가장 오래된 내비게이션 패턴이며, 닫혀있을 땐 앱 바에 햄버거 메뉴를 보여준다. 햄버거 메뉴를 탭 하면 왼쪽으로 메뉴 리스트가 보이게 된다. 네비게이션 드로어는 여러 목적지에 빠르게 접근할 때 유용하다.

단점은 햄버거 메뉴를 꼭 선택해야 한다는 것이다. 이에 반해 바텀, 탭 내비게이션 패턴은 화면 내 목적지들이 보인다. 하지만 반대로 내비게이션 드로어는 화면을 더 여유롭게 사용할 수 있다는 말이기도 하니 장점도 있다..

내가 보기엔 진짜 단점은 설정하기가 무지하게 복잡하고 내용이 많다는 것이다. 다 의미가 있는 행동이겠지만 더 쉽게 만들 수는 없었을까?

  1. 라이브러리 추가가 필요하다.
    • implementation(libs.androidx.navigation.fragment.ktx) : Fragment 를 내비게이션 목적지로 사용할 수 있게 함.
    • implementation(libs.androidx.navigation.ui.ktx) : App Bar, Bottom Navigation, Navigation Drawer UI Component
  2. AndroidManifest.xml 에 액션바 사용하지 않도록 android:theme 를 아래와 같이 설정한다.
<activity
    android:name=".MainActivity"
    android:exported="true"
    android:theme="@style/Theme.NavigationDrawer.NoActionBar">
  1. 내비게이션 그래프를 추가해야 한다. res 폴더에서 Command+N 으로 Resource Type 은 Navigation 으로 설정한 xml 파일 만들고 아래와 같이 만든다.
<?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>
  • fragment 태그의 android:id 와 android:name 으로 목적지를 설정하고 있다.
  • home 의 action 태그로 상세 화면으로 가는 액션을 생성했다.
  • 실제 그래프는 훨씬 길다. 일부분만 추가한 것이다.
  1. 위의 액션과 관련된 프로그래밍을 위해 HomeFragment 의 onCreateView 를 아래와 같이 수정한다.
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
}
  1. 내비게이션 그래프로 목적지도 설정했고 상세 화면 액션도 설정했지만 내비게이션 호스트가 필요하다. layout 폴더에 content_main.xml 을 생성한다.
<?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"/>
  • FragmentContainerView 를 생성한다. 목적지는 Fragment 다.
  • android:name 은 NavHostFragment 로 설정한다. 내비게이션 호스트가 될 것이다.
  • app:defaultNavHost 는 true. 기본 내비게이션 호스트다.
  • app:navGraph 는 3번에서 만든 @navigation/mobile_navigation 으로 설정해준다.
  1. 이제부터 drawer UI 설정이다. Drawer 의 헤더부터 설정한다.
<?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>
  1. Drawer 를 위해 제외한 앱 바를 대체하기 위해 새로운 앱 바 및 컨텐츠 레이아웃도 만든다.
<?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>
  1. 내비게이션 드로어 목록을 채운다. res 폴더에서 Command+N 으로 Resource Type 을 Menu 로 선택하고 activity_main_drawer.xml 을 생성한다.
<?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>
  • group 은 Drawer 내에 목록들 중 divider 로 나눠지는 기준이 될 것이다.
  • item 의 android:id 는 mobile_navigation.xml (navGraph, 내비게이션 그래프) 의 fragment 태그 android:id 와 일치한다.
  1. Overflow 메뉴 (맨 우측 세로로 ... 표시) 를 추가한다. res 폴더에서 Command+N 으로 Resource Type 을 Menu 로 설정한 뒤 main.xml 을 생성한다.
<?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>
  1. 내비게이션 드로어, 오버플로 메뉴, 앱 바(헤더 및 컨텐츠) 전부 연결하기 위해 이들을 activity_main.xml 에 추가한다.
<?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>
  1. 아직 오버플로 메뉴 설정과 드로어 상호작용이 남았다. MainActivity 를 세팅한다.
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))
    }
}
  • 햄버거 메뉴를 선택해서 나오는 메뉴들은 화면 이동은 되지만 햄버거 버튼이 뒤로가기 버튼으로 대체되지 않는다.
  • 하지만 아까 HomeFragment 에 추가한 버튼을 누르면 컨텐츠로 이동하게 되는데 이 땐 뒤로가기 버튼이 나온다.
  • 오버플로 버튼으로 이동한 화면도 뒤로가기 버튼이 나온다.

바텀 내비게이션

최상위 목적지 (primary destination) 이 적은 경우 유용하다. 앱의 보조 목적지 내에서 빠르게 항상 이용 가능한 내비게이션을 목적으로 사용된다.

  1. 의존성 주입, 내비게이션 그래프, Menu 리소스 파일 생성은 같다. 하지만 activity_main.xml 에서 추가해야 할 컴포넌트가 다르다.
<?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>
  1. 이를 MainActivity 에서 활성화한다.
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
        ))
    }
}
  • 내비게이션 drawer 와의 차이점은 AppBarConfiguration 에 네비게이션 drawer 관련 뷰를 넣지 않는다는 것이다.
  • onCreateOptionsMenu, onOptionsItemSelected 은 이전과 마찬가지로 main 메뉴를 오버플로 메뉴에 넣고, 프래그먼트 컨테이너 뷰를 통해 내비게이션 상태값을 관찰한다.

탭 내비게이션

관련 항목을 표시할 때 유용하다. 2~5개 사이 탭을 표시하고 그 이상의 탭은 스크롤로 처리한다.

주로 바텀 내비게이션과 함께 사용한다.

profile
plug-compatible programming unit

0개의 댓글