BottomNavigation

kotlin jetpack compose에서 Material3에서 BottomNavigation를 구현

BottomNavigation으로 보는 화면 이동

navigation

sealed Class

navigation으로 화면이동을 할 때 route를 설정하게 되는데 이는 보통 sealed Class로 설정하게 된다.

const val HOME = "homeScreen"
const val STATISTIC = "statisticScreen"
const val SETTING = "settingScreen"

sealed class BottomNavItem(
    val screenRoute: String,
    var bottomIcon: ImageVector,
    val bottomTitle: String,
) {
    data object HomeScreen : BottomNavItem(
        screenRoute = HOME,
        bottomIcon = Icons.Outlined.Home,
        bottomTitle = "Home",
    )
    data object StatisticScreen : BottomNavItem(
        screenRoute = STATISTIC,
        bottomIcon = Icons.Outlined.Equalizer,
        bottomTitle = "Statistics",
    )
    data object SettingScreen : BottomNavItem(
        screenRoute = SETTING,
        bottomIcon = Icons.Outlined.Person,
        bottomTitle = "Setting",
    )
}

NavHostnavController를 이용해서 실제 이동할 경로를 설정하는 컴포저블 함수이다.

@Composable
fun NavigationGraph(
    navController: NavHostController,
    recordViewModel: RecordViewModel,
    vicoViewModel: VicoViewModel
) {
    NavHost(navController = navController, startDestination = BottomNavItem.HomeScreen.screenRoute) {
        composable(BottomNavItem.HomeScreen.screenRoute) {
            if (!recordViewModel.isResult.value) {
                HomeScreen(viewModel = recordViewModel)
            } else {
                ResultScreen(viewModel = recordViewModel)
            }
        }
        composable(BottomNavItem.StatisticScreen.screenRoute) {
            StatisticesScreen(viewModel = vicoViewModel)
        }
        composable(BottomNavItem.SettingScreen.screenRoute) {

        }
    }
}

BottomNavigation

실제 사용자에게 보여질 BottomNavigation의 아이콘, 색상, 클릭시 이벤트를 설정하는 컴포저블 함수이다. 나중에 Scaffold에 등록하게 된다.

@Composable
fun BottomNavigation(navController: NavHostController, viewModel: RecordViewModel) {
    val items = listOf<BottomNavItem>(
        BottomNavItem.StatisticScreen,
        BottomNavItem.HomeScreen,
        BottomNavItem.SettingScreen,
    )
    val color = colorResource(id = R.color.border_grey)
    NavigationBar(
        modifier = Modifier
            .fillMaxWidth()
            .drawBehind {
                drawRoundRect(
                    color = color,
                    cornerRadius = CornerRadius(20.dp.toPx(), 20.dp.toPx()),
                    style = Stroke(width = 1f)
                )
            }
            .clip(shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp)),
        containerColor = Color.White,
        contentColor = Color.Black,
        tonalElevation = 100.dp
    ) {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentRoute = navBackStackEntry?.destination?.route
        items.forEach { item ->
            val selected = item.screenRoute == currentRoute
            NavigationBarItem(
                icon = {
                    Icon(
                        imageVector = item.bottomIcon,
                        contentDescription = item.bottomTitle,
                        modifier = Modifier.size(26.dp),
                    )
                },
                label = {
                    Text(
                        text = item.bottomTitle,
                        fontSize = 9.sp,
                    )
                },
                selected = selected,
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = Color(0xFF007AFF),
                    selectedTextColor = Color(0xFF007AFF),
                    unselectedIconColor = Color.Black,
                    unselectedTextColor = Color.Black,
                    indicatorColor = Color.White
                ),
                alwaysShowLabel = true,
                onClick = {
                    navController.navigate(item.screenRoute) {
                        navController.graph.startDestinationRoute?.let {
                            popUpTo(it) { saveState = true }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                },
            )
        }
    }
}

파란색은 NavigationBar, 빨간색은 NavigationBarItem로 알기 쉽게 사진으로 표현했다.

snackbarHostState

Modifier.drawBehind {} 은 BottomNavigation에서 위의 선을 그어 상단 UI와 구분 짓기 위해 사용했다.

아래는 현재 사용자에게 보여지는 화면의 라우터를 알려주는 코드이다.

val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Color 매개변수를 보면 indicatorColor를 설정한 것을 볼 수 있다. indicatorColor를 Color.White로 줘서 터치했을 때 반투명하게 나타나는 indicator 효과를 제거한 것이다.

navController.navigate(item.screenRoute) {
    navController.graph.startDestinationRoute?.let {
        popUpTo(it) { saveState = true }
    }
    launchSingleTop = true
    restoreState = true
}

onClick() 이벤트로 터치시 navController.navigate()를 통해 해당 라우터에 설정된 screen으로 이동한다. launchSingleTop = true를 통해 인스턴스 하나만 만들어지게 하였고, restoreState=true를 통해 버튼을 재클릭했을 때 이전 상태가 남아있게 했다.

MainActivity

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val recordViewModel by viewModels<RecordViewModel>()
    private val vicoViewModel by viewModels<VicoViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()

            WaterSaviorTheme {
                Scaffold(bottomBar = { BottomNavigation(navController = navController, viewModel = recordViewModel) }) {
                    Box(modifier = Modifier.padding(it)) {
                        NavigationGraph(recordViewModel = recordViewModel, vicoViewModel = vicoViewModel, navController = navController)
                    }
                }
            }
        }
    }
}

References

Material에서 BottomNavigation
Material3에서 BottomNavigation Indicator 효과 제거