[Android/Kotlin] Jetpack Navigation으로 Bottom Navigation 구현하기

코코아의 앱 개발일지·2025년 5월 22일
0

Android-Kotlin

목록 보기
38/39

안녕하세요, 또텀네비로 돌아왔습니다~!
바텀네비 관련해서만 벌써 몇 번쨰 포스트인지ㅎㅎ 모르겠네요.
그치만 할 때마다 조금씩 새롭게 하고 있어서,
오늘은 제가 거의 정착형으로 사용하고 있는 Jetpack Navigation을 활용한 구현 방법을 정리해보려 합니다.

Jetpack Navigation을 사용하지 않은 구현은 아래 포스트를 참고해주시면 됩니다.

🔗 커스텀 BottomNavigation 만들기


✍🏻 요구사항 분석

제가 구현할 바텀네비는 위 사진과 같았습니다.

뭐.. 기본적인 바텀네비게이션이랑 크게 다른 부분은 없으니, 요구사항은 아래와 같을 겁니다.

[요구사항]
1. 선택된 탭은 아이콘 & 텍스트 색상이 바뀐다
2. 선택된 탭에 따라 다른 화면을 보여줘야 한다

그나마 selected/unselected 아이콘은 아이콘 자체가 아니라 색상만 다르다는 점에서 구현이 조금 쉬워질 수 있겠네요.

💻 코드 작성

1️⃣ 의존성 추가

기존 Groovy DSL 말고 최근에 Kotlin DSL을 사용하고 있어서, 아래와 같이 버전 관리를 진행해 주었습니다.

libs.version.toml

[versions]
navigation = "2.9.0"

[libraries]
navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigation"}
navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigation"}

[bundles]
navigation = [
    "navigation-fragment-ktx",
    "navigation-ui-ktx"
]

모듈 수준의 gradle

dependencies {
    implementation(libs.bundles.navigation)
}

Groovy DSL을 사용한다면 모듈 수준의 gradle에 아래 코드를 바로 넣을 수 있습니다.

build.gradle (Project)

ext {
    navigationVersion = "2.7.7"
}

build.gradle (Module)

dependencies {
    implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion")
    implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
}

2️⃣ 아이콘 리소스 추가 + Fragment 생성

icon

svg로 추출한, 바텀네비에 들어가는 아이콘을 res > drawable 폴더 안에 추가해 줍니다.

select/unselect 아이콘의 디자인 자체가 다르다면 ic_nav_translator_selected, ic_nav_translator_unselected 식으로 아이콘을 2개를 추가해야겠지만ㅎㅎ 저는 선택 시 아이콘의 색상만 바뀌었기에 흰색으로 하나만 추가해 줬습니다.

selector

select와 unselect 시의 색상 변경을 위해 마찬가지로 res > drawable 폴더에 selector 코드를 작성합니다.

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:color="@color/white" android:state_checked="true" />
    <item android:color="#4DFFFFFF" />
</selector>

selected에서는 흰색, unselected에서는 투명도 30의 흰색이기에 state_checked에 따른 컬러 설정을 해줍니다. 색상은 #4DFFFFFF처럼 하드코딩으로 넣을 수도 있고, @color/white처럼 컬러 리소스의 색상을 불러올 수도 있습니다.

👇🏻 투명도 변환은 아래 아티클을 참고했습니다.
[Android] 투명도 - Hex값 정리

Fragment 코드 작성

각 탭을 선택했을 때 나오는 프래그먼트를 각각 만들어줍니다. (예: translator)

  • layout
  • fragment

약식으로 화면 이동이 잘 이루어지는지만 확인하고자 레이아웃에는 텍스트뷰 하나만 넣었습니다.

3️⃣ navGraph 추가

번역기, 퀴즈, 토론방, 트랜드 친구들을 '나 바텀네비에 속해있소!'하고 묶어주는 작업이라고도 볼 수 있을 거 같아요.
res > navigation 폴더에 nav_maintab이라는 이름으로 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/nav_maintab"
    app:startDestination="@id/translatorFragment">

    <!-- Translator -->
    <fragment
        android:id="@+id/translatorFragment"
        android:name="com.nahyun.mz.ui.translator.TranslatorFragment"
        android:label="TranslatorFragment"
        tools:layout="@layout/fragment_translator"/>

    <!-- Quiz -->
    <fragment
        android:id="@+id/quizFragment"
        android:name="com.nahyun.mz.ui.quiz.QuizFragment"
        android:label="QuizFragment"
        tools:layout="@layout/fragment_quiz"/>

    <!-- Discussion -->
    <fragment
        android:id="@+id/discussionFragment"
        android:name="com.nahyun.mz.ui.discussion.DiscussionFragment"
        android:label="DiscussionFragment"
        tools:layout="@layout/fragment_discussion"/>

    <!-- Trend -->
    <fragment
        android:id="@+id/trendFragment"
        android:name="com.nahyun.mz.ui.trend.TrendFragment"
        android:label="TrendFragment"
        tools:layout="@layout/fragment_trend"/>

</navigation>

navigation의 id를 지정해주고, 아래에 해당 네비게이션에 들어가는 프래그먼트를 적어줍니다.
startDestination는 아래에 적은 프래그먼트 중, 시작점으로 설정할 프래그먼트의 id를 적어주면 됩니다.

4️⃣ menu 추가

res > 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/translatorFragment"
        android:icon="@drawable/ic_nav_translator"
        android:title="@string/menu_translator"
        android:enabled="true"
        app:showAsAction="always"/>

    <item
        android:id="@+id/quizFragment"
        android:icon="@drawable/ic_nav_quiz"
        android:title="@string/menu_quiz"
        android:enabled="true"
        app:showAsAction="ifRoom"/>

    <item
        android:id="@+id/discussionFragment"
        android:icon="@drawable/ic_nav_discussion"
        android:title="@string/menu_discussion"
        android:enabled="true"
        app:showAsAction="ifRoom"/>

    <item
        android:id="@+id/trendFragment"
        android:icon="@drawable/ic_nav_trend"
        android:title="@string/menu_trend"
        android:enabled="true"
        app:showAsAction="always"/>
</menu>

id는 이전 navigation에 작성했던 fragment의 id로 적어줍니다.
아이템 안에는 아이콘, 타이틀(탭 이름)을 적어줍니다.

5️⃣ BottomNavigationView 추가

이제 바텀네비가 들어갈 화면인, MainActivity에서 코드를 작성해 줄 차례입니다.

layout

액티비티 하단에 material의 BottomNavigationView로 바텀네비의 영역을 만들어줍니다.

<com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/main_nav_bar"
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:background="@color/main"
            android:paddingHorizontal="25dp"
            app:itemIconTint="@drawable/selector_nav_color"
            app:itemTextColor="@drawable/selector_nav_color"
            app:menu="@menu/nav_menu"
            app:itemIconSize="30dp"
            app:labelVisibilityMode="labeled"
            app:itemActiveIndicatorStyle="@android:color/transparent"
            app:itemRippleColor="@null"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/main_nav_host" />

배경색, 아이콘 사이즈, 라벨 표시 여부, 리플 효과 등 코드를 지정할 수 있습니다.

app:itemIconTint="@drawable/selector_nav_color"
app:itemTextColor="@drawable/selector_nav_color"

여기에 아까 2번 단계에서 만들어준 selector 코드를 넣는 것이 아이콘/텍스트 색상 변경의 핵심입니다.

바텀네비 위는 선택한 탭에 따라 프래그먼트 교체를 해주어야 하는 영역입니다.

<fragment
            android:id="@+id/main_nav_host"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:navGraph="@navigation/nav_maintab"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/main_nav_bar"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

navGraph로 3에서 만들었던 nagigation의 id를 적어줍니다.

activity_main.xml의 전체 코드는 아래와 같습니다.

<?xml version="1.0" encoding="utf-8"?>
<layout
    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">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true"
        tools:context=".MainActivity">

        <fragment
            android:id="@+id/main_nav_host"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="match_parent"
            android:layout_height="0dp"
            app:navGraph="@navigation/nav_maintab"
            app:defaultNavHost="true"
            app:layout_constraintBottom_toTopOf="@id/main_nav_bar"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/main_nav_bar"
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:background="@color/main"
            android:paddingHorizontal="25dp"
            app:itemIconTint="@drawable/selector_nav_color"
            app:itemTextColor="@drawable/selector_nav_color"
            app:menu="@menu/nav_menu"
            app:itemIconSize="30dp"
            app:labelVisibilityMode="labeled"
            app:itemActiveIndicatorStyle="@android:color/transparent"
            app:itemRippleColor="@null"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/main_nav_host" />

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

6️⃣ BottomBar + NavController 연결

이제 마지막으로 선택된 탭이랑 navController를 연결해 줄 차례입니다.
액티비티 코드 안에 추가해야하는 코드는 간단합니다.

private fun initNavigation() {
	NavigationUI.setupWithNavController(binding.mainNavBar, findNavController(R.id.main_nav_host))
}

setupWithNavController에 navigationBarView, navController를 넣어 선택 탭에 따라 프래그먼트도 바뀔 수 있게 합니다.
위 코드를 추가하지 않으면 바텀네비 아이템을 선택했을 때 탭의 select 상태는 변경되지만, 바텀바 위의 프래그먼트는 바뀌지 않게 됩니다.

이렇게 작성한 initNavigation() 함수를 MainActivity의 onCreate 안에 넣어줍니다.

📱 완성 화면

실행해보면 선택한 탭에 따라 프래그먼트가 잘 바뀌는 모습을 확인할 수 있습니다~!



번외) 만약 아이콘 자체가 selected/unselected 아이콘으로 나뉘어진다면?

프로젝트를 하다보면 종종 이렇게 선택/미선택 시의 아이콘 디자인 자체가 달라지는 경우도 보실 텐데요,

아이콘 자체가 다른 경우색상 커스텀이 어려운 경우

그럴 땐 selected/unselected 아이콘 두 개를 모두 drawable에 추가하고,
selector의 drawable에 아이콘 자체를 넣어서 state_checked에 대한 처리를 해줄 수도 있습니다. (2번 과정)

그리고 menu 코드를 작성할 때, icon의 drawable에 위에서 만든 selector 자체를 넣어줍니다. 이 selector도 바텀네비에 들어갈 아이템마다 추가해서 넣어주면 됩니다. (4번 과정)

아이콘 이미지 자체를 이미 menu에서 바꾸어주었기 때문에,
activity_main.xml에서 BottomNavigationView 코드를 작성할 떄 itemIconTint는 따로 설정할 필요가 없습니다. (5번 과정)

언급한 부분 외에 나머지 코드는 동일합니다.



‼️ 마치며

오늘은 이렇게 Jetpack Navigation으로 바텀바를 구현하는 방법에 대해 정리해 보았는데요,
Jetpack Navigation을 활용하면 어떤 이점이 있느냐! 가 궁금하실 수 있을 것 같아요.

🔗 커스텀 BottomNavigation 만들기

포스트 초반에 언급한, Jetpack Navigation을 사용하지 않고 바텀바를 구현했을 때와 비교해보면, 3번과 6번 과정이 주요한 차이점인데요.
navGraph와 BottomNavigationView를 아래 코드 하나만으로 손쉽게 연결해줌으로써 관리가 무척 편리해집니다.
<Jetpack Navigation 활용 시 코드>

NavigationUI.setupWithNavController(binding.mainNavBar, findNavController(R.id.main_nav_host))

Jetpack Navigation 사용 전에는 MainActivity에 아래와 같이 상당히 긴 코드가 들어갔거든요.
아이템이 클릭되었을 때 어떤 프래그먼트를 보여줄 것인지, 최초 프래그먼트는 뭘로 할지.. Jetpack Navigation에서는 navGraphsetupWithNavController에서 정의해줄 수 있는 부분을 액티비티 코드에서 직접 다뤄줬어야 했습니다.
<Jetpack Navigation 활용X 코드>

private fun initBottomNav() {
        binding.mainLayoutBottomNavigation.itemIconTintList = null

        binding.mainLayoutBottomNavigation.setOnItemSelectedListener {
            when(it.itemId) {
                R.id.main_bottom_nav_home -> {
                    HomeFragment().changeFragment()
                }

                R.id.main_bottom_nav_friends -> {
                    FriendsFragment().changeFragment()
                }

                R.id.main_bottom_nav_record -> {
                    RecordFragment().changeFragment()
                }
            }
            return@setOnItemSelectedListener true
        }

        binding.mainLayoutBottomNavigation.setOnItemReselectedListener {  } // 바텀네비 재클릭시 화면 재생성 방지
    }

private fun Fragment.changeFragment() {
        manager.beginTransaction().replace(R.id.main_layout_container, this).commit()
}

fun showInit() {
        val transaction = manager.beginTransaction()
            .add(R.id.main_layout_container, HomeFragment())
        transaction.commit()
}

이번 포스트에서는 Jetpack Navigation으로 BottomNavigation을 구현하는 코드만 집중적으로 살펴봤는데요, Jetpack Navigation의 사용 장점으로 이게 끝이라면 '굳이 왜 사용해야할까?'하는 부분이 크게 와닿지 않을 수도 있어요.

당연합니다!
이번 포스트에서는 navGraph를 어떤 식으로 활용할 수 있는지에 대해서는 다루지 않았으니까요ㅎㅎ

Jetpack Navigation을 사용하면 한 액티비티 내에서 화면 이동이 이루어지는, 모든 과정을 navGraph 하나로도 손쉽게 관리하는 게 가능합니다.
화면 이동 시에 bundle이나 intent를 이용하지 않고도 데이터를 쉽게 전달하는 방법도 있고요.

아무튼 잘 사용하면 정말 편한 친구입니다ㅎㅎ (제가 그만큼 잘 사용하고 있는지는 자신이 없지만요)

모든 내용을 하나의 포스트에 다 다루기에는 무리가 있으니, 이번에는 딱 Bottom Navigation 내용만 정리해 보았는데요,
Jetpack Navigation 개념과 활용은 조만간 다른 포스트로 찾아뵙겠습니다!

읽어주셔서 감사합니다.



📚 참고 자료

profile
안드로이드 개발자를 꿈꾸는 학생입니다

2개의 댓글

comment-user-thumbnail
2025년 5월 24일

작년쯤에 UMC 컨퍼런스에서 Fragment 백스택 관련해서 발표하신거 잘 들었었는데 velog도 운영하고 계셨군요!!

1개의 답글