컴포즈 예제를 하나씩 하던 중 무조건 그 어느 프로젝트를 하던 해야하는 BottomNavigation만들기를 도전해보았다.
1단계
dependency 정의
//navigation
implementation("androidx.navigation:navigation-compose:2.4.2")
Jetpack Navigation을 사용할때와 비슷하게 app단 gradle에 정의를 해준다.
2단계
이동할 화면들 정의
네비게이션 item으로 이동할 화면은 총 5개이므로 5개의 화면을 정의해준다.
@Composable
fun HomeScreen(){
Box(modifier = Modifier
.fillMaxSize(),
contentAlignment = Alignment.Center){
Text(text = "HomeScreen",
fontSize = 22.sp)
}
}
여기서는 5개중 하나의 화면의 코드만 가지고 왔다. 다른 화면들은 화면이 이동되었다는 것을 시각적으로 보여줄 수 있도록 단순하게 text만 변경하여 정의하였다.
3단계
네비게이션 화면들 enum class로 정의하기
enum class Screens {
HomeScreen,
CommunityScreen,
FundingScreen,
FamilyScreen,
MypageScreen
}
네비게이션 라우트들의 이름을 하나의 enum class로 정의하여 한 곳에서 관리할 수 있도록 하였다.
4단계
BottomNaviItem만들어주기
XML방식의 menu와 같은 역할을 하는 것을 컴포즈로 만들어줘야만 해당 item의 text와 icon을 할당해줄 수 있다. 하단 네비게이션에 들어갈 아이템들을 만들어주기 위해 data class를 생성해주도록 하자.
data class NavItem(
val label:String,
val icon_unfill: Int,
val route: String
)
val listOfNavItem : List<NavItem> = listOf(
NavItem(
label = "홈",
icon_unfill = R.drawable.ic_home,
route = Screens.HomeScreen.name
),
NavItem(
label = "커뮤니티",
icon_unfill = R.drawable.ic_community,
route = Screens.CommunityScreen.name
),
NavItem(
label = "가족",
icon_unfill = R.drawable.ic_family,
route = Screens.FamilyScreen.name
),
NavItem(
label = "후원",
icon_unfill = R.drawable.ic_funding,
route = Screens.FundingScreen.name
),
NavItem(
label = "마이페이지",
icon_unfill = R.drawable.ic_my,
route = Screens.MypageScreen.name
),
)
다섯개의 NavItem이 정의되어 있다.
5단계
이제 가장 중요한 단계이다!
이제 본격적으로 BottomNavigationBar을 디자인하고 로직을 넣어줄 차례이다.
제일 먼저 NavController을 만들어준다. 모든 컴포저블이 접근할 수 있어야하는 곳이어야 하기에 맨위 그리고 맨 처음으로 정의해준다.
val navController : NavHostController = rememberNavController()
위와 같이 정의해주면 된다.
material3에서는 bottomnavibar을 만들기 위해서 Scaffold안에 bottomBar 그안에 NavigationBar 그 안에 NavigationBarItem을 정의해주어야한다.
var selectedIndex by rememberSaveable{ mutableStateOf(0) }
이는 현재 선택된 네비게이션 아이템의 인덱스를 저장하는 상태이다. rememberSaveable로 상태가 유지되도록 하였다.
NavigationBar(
containerColor = Color.White,
contentColor = Color.White,
modifier = Modifier.height(66.dp)
)
다음으로 하단에 탭을 표시하기 위한 바를 정의한다. 위와 같이 높이와 배경색을 정의해준다.
Scaffold(
bottomBar = {
var selectedIndex by rememberSaveable{ mutableStateOf(0) }
NavigationBar(
containerColor = Color.White,
contentColor = Color.White,
modifier = Modifier.height(66.dp)
) {
listOfNavItem.forEachIndexed{index,navItem: NavItem ->
NavigationBarItem(
selected = selectedIndex == index,
onClick = {
selectedIndex = index
navController.navigate(navItem.route){
popUpTo(0){
inclusive = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(
painter = painterResource(id = navItem.icon_unfill),
contentDescription = null,
modifier = Modifier
.width(24.dp)
.height(24.dp)
.padding(bottom = 4.dp),
tint = if(selectedIndex == index){
Color(ContextCompat.getColor(context, R.color.orange_500))
}else{
Color(ContextCompat.getColor(context, R.color.secondary_200))
}
)
},
label={
Text(
text = navItem.label,
fontSize = 10.sp,
color = if(selectedIndex == index){
Color(ContextCompat.getColor(context, R.color.orange_500))
}else{
Color(ContextCompat.getColor(context, R.color.secondary_200))
}
)
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = Color.White
)
)
}
}
}
)
이 코드에 대해 하나하나 설명을 해보겠다.
paddingValues ->
NavHost(
navController = navController,
startDestination = Screens.HomeScreen.name,
modifier = Modifier.padding(paddingValues)
){
composable(route = Screens.HomeScreen.name){
HomeScreen()
}
composable(route = Screens.CommunityScreen.name){
CommunityScreen()
}
composable(route = Screens.FamilyScreen.name){
FamilyScreen()
}
composable(route = Screens.FundingScreen.name){
FundingScreen()
}
composable(route = Screens.MypageScreen.name){
MypageScreen()
}
}
}
paddingValues를 사용해서 Scaffold 내부의 content 영역이 다른 UI요소들과 겹치지 않도록 해주었다. navhosts는 네비게이션 그래프와 NavController를 연결한다.
(참고: navhost를 만들기 위해서는 rememberNavController을 통해 만든 navController가 필요하다. 또한 그래프의 시작 대상의 경로가 필요하다.)
6단계
MainActivity에 만들어준 네비게이션 바와 로직을 포함시켜준다.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BottomNavigationTheme {
AppNavigation()
}
}
}
}
완성!
드디어 이쁜 네비바가 완성되었다.
아래는 이 기능에서 가장 중요한 역할을 하는 AppNavigation의 전체 코드이다.
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppNavigation(){
val navController : NavHostController = rememberNavController()
val context = LocalContext.current
Scaffold(
bottomBar = {
var selectedIndex by rememberSaveable{ mutableStateOf(0) }
NavigationBar(
containerColor = Color.White,
contentColor = Color.White,
modifier = Modifier.height(66.dp)
) {
listOfNavItem.forEachIndexed{index,navItem: NavItem ->
NavigationBarItem(
selected = selectedIndex == index,
onClick = {
selectedIndex = index
navController.navigate(navItem.route){
popUpTo(0){
inclusive = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
Icon(
painter = painterResource(id = navItem.icon_unfill),
contentDescription = null,
modifier = Modifier
.width(24.dp)
.height(24.dp)
.padding(bottom = 4.dp),
tint = if(selectedIndex == index){
Color(ContextCompat.getColor(context, R.color.orange_500))
}else{
Color(ContextCompat.getColor(context, R.color.secondary_200))
}
)
},
label={
Text(
text = navItem.label,
fontSize = 10.sp,
color = if(selectedIndex == index){
Color(ContextCompat.getColor(context, R.color.orange_500))
}else{
Color(ContextCompat.getColor(context, R.color.secondary_200))
}
)
},
colors = NavigationBarItemDefaults.colors(
indicatorColor = Color.White
)
)
}
}
}
){paddingValues ->
NavHost(
navController = navController,
startDestination = Screens.HomeScreen.name,
modifier = Modifier.padding(paddingValues)
){
composable(route = Screens.HomeScreen.name){
HomeScreen()
}
composable(route = Screens.CommunityScreen.name){
CommunityScreen()
}
composable(route = Screens.FamilyScreen.name){
FamilyScreen()
}
composable(route = Screens.FundingScreen.name){
FundingScreen()
}
composable(route = Screens.MypageScreen.name){
MypageScreen()
}
}
}
}