안녕하세요, 또텀네비로 돌아왔습니다~!
바텀네비 관련해서만 벌써 몇 번쨰 포스트인지ㅎㅎ 모르겠네요.
그치만 할 때마다 조금씩 새롭게 하고 있어서,
오늘은 제가 거의 정착형으로 사용하고 있는 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도 운영하고 계셨군요!!