안녕하세요, 또텀네비로 돌아왔습니다~!
바텀네비 관련해서만 벌써 몇 번쨰 포스트인지ㅎㅎ 모르겠네요.
그치만 할 때마다 조금씩 새롭게 하고 있어서,
오늘은 제가 거의 정착형으로 사용하고 있는 Jetpack Navigation을 활용한 구현 방법을 정리해보려 합니다.
Jetpack Navigation을 사용하지 않은 구현은 아래 포스트를 참고해주시면 됩니다.
제가 구현할 바텀네비는 위 사진과 같았습니다.
뭐.. 기본적인 바텀네비게이션이랑 크게 다른 부분은 없으니, 요구사항은 아래와 같을 겁니다.
[요구사항]
1. 선택된 탭은 아이콘 & 텍스트 색상이 바뀐다
2. 선택된 탭에 따라 다른 화면을 보여줘야 한다
그나마 selected/unselected 아이콘은 아이콘 자체가 아니라 색상만 다르다는 점에서 구현이 조금 쉬워질 수 있겠네요.
기존 Groovy DSL 말고 최근에 Kotlin DSL을 사용하고 있어서, 아래와 같이 버전 관리를 진행해 주었습니다.
[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"
]
dependencies {
implementation(libs.bundles.navigation)
}
Groovy DSL을 사용한다면 모듈 수준의 gradle에 아래 코드를 바로 넣을 수 있습니다.
ext {
navigationVersion = "2.7.7"
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:$navigationVersion")
implementation "androidx.navigation:navigation-ui-ktx:$navigationVersion"
}
svg로 추출한, 바텀네비에 들어가는 아이콘을 res > drawable 폴더 안에 추가해 줍니다.

select/unselect 아이콘의 디자인 자체가 다르다면 ic_nav_translator_selected, ic_nav_translator_unselected 식으로 아이콘을 2개를 추가해야겠지만ㅎㅎ 저는 선택 시 아이콘의 색상만 바뀌었기에 흰색으로 하나만 추가해 줬습니다.
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값 정리
각 탭을 선택했을 때 나오는 프래그먼트를 각각 만들어줍니다. (예: translator)


약식으로 화면 이동이 잘 이루어지는지만 확인하고자 레이아웃에는 텍스트뷰 하나만 넣었습니다.
번역기, 퀴즈, 토론방, 트랜드 친구들을 '나 바텀네비에 속해있소!'하고 묶어주는 작업이라고도 볼 수 있을 거 같아요.
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를 적어주면 됩니다.

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로 적어줍니다.
아이템 안에는 아이콘, 타이틀(탭 이름)을 적어줍니다.

이제 바텀네비가 들어갈 화면인, MainActivity에서 코드를 작성해 줄 차례입니다.
액티비티 하단에 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>
이제 마지막으로 선택된 탭이랑 navController를 연결해 줄 차례입니다.
액티비티 코드 안에 추가해야하는 코드는 간단합니다.
private fun initNavigation() {
NavigationUI.setupWithNavController(binding.mainNavBar, findNavController(R.id.main_nav_host))
}
setupWithNavController에 navigationBarView, navController를 넣어 선택 탭에 따라 프래그먼트도 바뀔 수 있게 합니다.
위 코드를 추가하지 않으면 바텀네비 아이템을 선택했을 때 탭의 select 상태는 변경되지만, 바텀바 위의 프래그먼트는 바뀌지 않게 됩니다.
이렇게 작성한 initNavigation() 함수를 MainActivity의 onCreate 안에 넣어줍니다.
실행해보면 선택한 탭에 따라 프래그먼트가 잘 바뀌는 모습을 확인할 수 있습니다~!
프로젝트를 하다보면 종종 이렇게 선택/미선택 시의 아이콘 디자인 자체가 달라지는 경우도 보실 텐데요,
![]() | ![]() |
|---|---|
| 아이콘 자체가 다른 경우 | 색상 커스텀이 어려운 경우 |
그럴 땐 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을 활용하면 어떤 이점이 있느냐! 가 궁금하실 수 있을 것 같아요.
포스트 초반에 언급한, Jetpack Navigation을 사용하지 않고 바텀바를 구현했을 때와 비교해보면, 3번과 6번 과정이 주요한 차이점인데요.
navGraph와 BottomNavigationView를 아래 코드 하나만으로 손쉽게 연결해줌으로써 관리가 무척 편리해집니다.
<Jetpack Navigation 활용 시 코드>
NavigationUI.setupWithNavController(binding.mainNavBar, findNavController(R.id.main_nav_host))
Jetpack Navigation 사용 전에는 MainActivity에 아래와 같이 상당히 긴 코드가 들어갔거든요.
아이템이 클릭되었을 때 어떤 프래그먼트를 보여줄 것인지, 최초 프래그먼트는 뭘로 할지.. Jetpack Navigation에서는 navGraph랑 setupWithNavController에서 정의해줄 수 있는 부분을 액티비티 코드에서 직접 다뤄줬어야 했습니다.
<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 개념과 활용은 조만간 다른 포스트로 찾아뵙겠습니다!
읽어주셔서 감사합니다.
작년쯤에 UMC 컨퍼런스에서 Fragment 백스택 관련해서 발표하신거 잘 들었었는데 velog도 운영하고 계셨군요!!