-
Notifications
You must be signed in to change notification settings - Fork 0
[Feat] BottomNavigationBar Compose 마이그레이션 #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e9de57c
007ef66
75d7655
180304f
fef77a7
5d62e39
9da1eb0
b99cdad
4cf0aa2
48a97e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.daedan.festabook.presentation.home.navigation | ||
|
|
||
| import androidx.compose.foundation.layout.PaddingValues | ||
| import androidx.compose.foundation.layout.padding | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.navigation.NavGraphBuilder | ||
| import androidx.navigation.compose.composable | ||
| import com.daedan.festabook.presentation.home.HomeViewModel | ||
| import com.daedan.festabook.presentation.home.component.HomeScreen | ||
| import com.daedan.festabook.presentation.main.MainTabRoute | ||
|
|
||
| fun NavGraphBuilder.homeNavGraph( | ||
| padding: PaddingValues, | ||
| viewModel: HomeViewModel, | ||
| onNavigateToExplore: () -> Unit, | ||
| ) { | ||
| composable<MainTabRoute.Home> { | ||
| HomeScreen( | ||
| modifier = Modifier.padding(padding), | ||
| viewModel = viewModel, | ||
| onNavigateToExplore = onNavigateToExplore, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.daedan.festabook.presentation.main | ||
|
|
||
| import androidx.annotation.DrawableRes | ||
| import androidx.annotation.StringRes | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.ReadOnlyComposable | ||
| import com.daedan.festabook.R | ||
| import com.daedan.festabook.presentation.theme.FestabookColor | ||
|
|
||
| enum class FestabookMainTab( | ||
| @param:DrawableRes val iconResId: Int, | ||
| @param:StringRes val labelResId: Int, | ||
| val route: MainTabRoute, | ||
| ) { | ||
| HOME( | ||
| iconResId = R.drawable.ic_home, | ||
| labelResId = R.string.menu_home_title, | ||
| route = MainTabRoute.Home, | ||
| ), | ||
| SCHEDULE( | ||
| iconResId = R.drawable.ic_schedule, | ||
| labelResId = R.string.menu_schedule_title, | ||
| route = MainTabRoute.Schedule, | ||
| ), | ||
| PLACE_MAP( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 메뉴는 MAP으로 네이밍해도 괜찮지 않을까요?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 저는 패키지 네이밍을 따라가는 것이 더 직관적이라고 생각했습니다..! |
||
| iconResId = R.drawable.ic_map, | ||
| labelResId = R.string.menu_map_title, | ||
| route = MainTabRoute.PlaceMap, | ||
| ), | ||
| NEWS( | ||
| iconResId = R.drawable.ic_news, | ||
| labelResId = R.string.menu_news_title, | ||
| route = MainTabRoute.News, | ||
| ), | ||
| SETTING( | ||
| iconResId = R.drawable.ic_setting, | ||
| labelResId = R.string.menu_setting_title, | ||
| route = MainTabRoute.Setting, | ||
| ), | ||
| ; | ||
|
|
||
| companion object Defaults { | ||
| @Composable | ||
| fun find(predicate: @Composable (FestabookRoute) -> Boolean) = | ||
| entries.find { | ||
| predicate(it.route) | ||
| } | ||
|
|
||
| val selectedColor | ||
| @Composable | ||
| @ReadOnlyComposable | ||
| get() = FestabookColor.black | ||
|
|
||
| val unselectedColor | ||
| @Composable | ||
| @ReadOnlyComposable | ||
| get() = FestabookColor.gray400 | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package com.daedan.festabook.presentation.main | ||
|
|
||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.remember | ||
| import androidx.navigation.NavDestination.Companion.hasRoute | ||
| import androidx.navigation.NavGraph.Companion.findStartDestination | ||
| import androidx.navigation.NavHostController | ||
| import androidx.navigation.NavOptions | ||
| import androidx.navigation.compose.currentBackStackEntryAsState | ||
| import androidx.navigation.compose.rememberNavController | ||
| import androidx.navigation.navOptions | ||
|
|
||
| class FestabookNavigator( | ||
| val navController: NavHostController, | ||
| ) { | ||
| private val currentDestination | ||
| @Composable | ||
| get() = | ||
| navController | ||
| .currentBackStackEntryAsState() | ||
| .value | ||
| ?.destination | ||
|
|
||
| private val defaultNavOptions = | ||
| navOptions { | ||
| popUpTo(navController.graph.findStartDestination().id) { | ||
| saveState = true | ||
| } | ||
| launchSingleTop = true | ||
| restoreState = true | ||
| } | ||
|
Comment on lines
+24
to
+31
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 해당 네비게이션 옵션을
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 공통적으로 쓰일 확률이 높아 옵션을 재사용하기 위해 이렇게 구성했습니다. |
||
|
|
||
| val currentTab | ||
| @Composable | ||
| get() = | ||
| FestabookMainTab.find { | ||
| currentDestination?.hasRoute(it::class) ?: false | ||
| } | ||
|
|
||
| val shouldShowBottomBar | ||
| @Composable | ||
| get() = | ||
| FestabookMainTab.find { | ||
| currentDestination?.hasRoute(it::class) ?: false | ||
| } != null | ||
|
|
||
| val startRoute = MainTabRoute.Home // TODO: Splash와 Explore 연동 시 변경 | ||
|
|
||
| fun navigateToMainTab(route: FestabookRoute) { | ||
| navController.navigate( | ||
| route, | ||
| defaultNavOptions, | ||
| ) | ||
| } | ||
|
|
||
| fun navigate( | ||
| route: FestabookRoute, | ||
| navOptions: NavOptions? = null, | ||
| ) { | ||
| navController.navigate( | ||
| route, | ||
| navOptions, | ||
| ) | ||
| } | ||
|
|
||
| fun popBackStack() { | ||
| navController.popBackStack() | ||
| } | ||
|
|
||
| fun popBackStack( | ||
| route: FestabookRoute, | ||
| inclusive: Boolean = false, | ||
| ) { | ||
| navController.popBackStack(route, inclusive) | ||
| } | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 뒤로가기 기능을 하는 함수도 만들어두면 좋을 것 같아요 ~
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| @Composable | ||
| fun rememberFestabookNavigator(): FestabookNavigator { | ||
| val navController = rememberNavController() | ||
| return remember { | ||
| FestabookNavigator(navController = navController) | ||
| } | ||
| } | ||
|
Comment on lines
+78
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 함수로 따로 빼신 이유가 궁금합니다 !
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. navController와 생명주기를 동일하게 가져가도록 보장하고 싶었기 때문입니다 ! |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| package com.daedan.festabook.presentation.main | ||
|
|
||
| import kotlinx.serialization.Serializable | ||
|
|
||
| @Serializable | ||
| sealed interface FestabookRoute { | ||
| @Serializable | ||
| data object Splash : FestabookRoute | ||
|
|
||
| // TODO: PlaceUiModel, PlaceDetailUiModel 생성자에 추가 후 UiModel에 @Serializable 어노테이션 필요 | ||
| @Serializable | ||
| data object PlaceDetail : FestabookRoute | ||
|
|
||
| @Serializable | ||
| data object Explore : FestabookRoute | ||
| } | ||
|
|
||
| @Serializable | ||
| sealed interface MainTabRoute : FestabookRoute { | ||
| @Serializable | ||
| data object Home : MainTabRoute | ||
|
|
||
| @Serializable | ||
| data object Schedule : MainTabRoute | ||
|
|
||
| @Serializable | ||
| data object PlaceMap : MainTabRoute | ||
|
|
||
| @Serializable | ||
| data object News : MainTabRoute | ||
|
|
||
| @Serializable | ||
| data object Setting : MainTabRoute | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package com.daedan.festabook.presentation.main.component | ||
|
|
||
| import androidx.compose.foundation.Image | ||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.clickable | ||
| import androidx.compose.foundation.interaction.MutableInteractionSource | ||
| import androidx.compose.foundation.layout.Arrangement | ||
| import androidx.compose.foundation.layout.Box | ||
| import androidx.compose.foundation.layout.Column | ||
| import androidx.compose.foundation.layout.Row | ||
| import androidx.compose.foundation.layout.RowScope | ||
| import androidx.compose.foundation.layout.Spacer | ||
| import androidx.compose.foundation.layout.fillMaxHeight | ||
| import androidx.compose.foundation.layout.fillMaxWidth | ||
| import androidx.compose.foundation.layout.height | ||
| import androidx.compose.foundation.layout.offset | ||
| import androidx.compose.foundation.selection.selectable | ||
| import androidx.compose.material3.Icon | ||
| import androidx.compose.material3.Scaffold | ||
| import androidx.compose.material3.Text | ||
| import androidx.compose.runtime.Composable | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.ui.Alignment | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.res.painterResource | ||
| import androidx.compose.ui.res.stringResource | ||
| import androidx.compose.ui.tooling.preview.Preview | ||
| import androidx.compose.ui.unit.dp | ||
| import com.daedan.festabook.R | ||
| import com.daedan.festabook.presentation.main.FestabookMainTab | ||
| import com.daedan.festabook.presentation.main.FestabookMainTab.Defaults.selectedColor | ||
| import com.daedan.festabook.presentation.main.FestabookMainTab.Defaults.unselectedColor | ||
| import com.daedan.festabook.presentation.theme.FestabookColor | ||
| import com.daedan.festabook.presentation.theme.FestabookTheme | ||
| import com.daedan.festabook.presentation.theme.FestabookTypography | ||
| import com.daedan.festabook.presentation.theme.festabookSpacing | ||
|
|
||
| @Composable | ||
| fun FestabookBottomNavigationBar( | ||
| currentTab: FestabookMainTab?, | ||
| onTabSelect: (FestabookMainTab) -> Unit, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Box( | ||
| contentAlignment = Alignment.BottomCenter, | ||
| modifier = modifier, | ||
| ) { | ||
| Row( | ||
| modifier = | ||
| Modifier | ||
| .background(color = FestabookColor.white) | ||
| .fillMaxWidth() | ||
| .height(70.dp), | ||
| ) { | ||
| FestabookMainTab.entries.forEach { item -> | ||
| when (item) { | ||
| FestabookMainTab.PLACE_MAP -> Spacer(modifier = Modifier.weight(1f)) | ||
| else -> { | ||
| FestabookNavigationItem( | ||
| tab = item, | ||
| selected = item == currentTab, | ||
| onClick = onTabSelect, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| PlaceMapNavigationItem(onClick = onTabSelect) | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun RowScope.FestabookNavigationItem( | ||
| onClick: (FestabookMainTab) -> Unit, | ||
| tab: FestabookMainTab, | ||
| selected: Boolean, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Box( | ||
| modifier = | ||
| modifier | ||
| .weight(1f) | ||
| .fillMaxHeight() | ||
| .background(color = FestabookColor.white) | ||
| .selectable( | ||
| selected = selected, | ||
| onClick = { onClick(tab) }, | ||
| interactionSource = remember { MutableInteractionSource() }, | ||
| indication = null, // 리플(Ripple) 완벽 제거 | ||
| ), | ||
| contentAlignment = Alignment.Center, | ||
| ) { | ||
| Column( | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| verticalArrangement = Arrangement.Center, | ||
| ) { | ||
| Icon( | ||
| painter = painterResource(id = tab.iconResId), | ||
| contentDescription = stringResource(tab.labelResId), | ||
| tint = if (selected) selectedColor else unselectedColor, | ||
| ) | ||
| Spacer(modifier = Modifier.height(festabookSpacing.paddingBody1)) | ||
| Text( | ||
| text = stringResource(tab.labelResId), | ||
| style = FestabookTypography.labelMedium, | ||
| color = if (selected) selectedColor else unselectedColor, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Composable | ||
| private fun PlaceMapNavigationItem( | ||
| onClick: (FestabookMainTab) -> Unit, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| Image( | ||
| modifier = | ||
| modifier | ||
| .offset(y = -festabookSpacing.paddingBody4) | ||
| .clickable( | ||
| onClick = { | ||
| onClick(FestabookMainTab.PLACE_MAP) | ||
| }, | ||
| indication = null, | ||
| interactionSource = remember { MutableInteractionSource() }, | ||
| ), | ||
| painter = painterResource(id = R.drawable.btn_fab_manu), | ||
| contentDescription = stringResource(FestabookMainTab.PLACE_MAP.labelResId), | ||
| ) | ||
| } | ||
|
|
||
| @Preview(showBackground = true) | ||
| @Composable | ||
| private fun FestabookBottomNavigationBarPreview() { | ||
| FestabookTheme { | ||
| var currentTabState by remember { mutableStateOf(FestabookMainTab.HOME) } | ||
| Scaffold( | ||
| bottomBar = { | ||
| FestabookBottomNavigationBar( | ||
| currentTab = currentTabState, | ||
| onTabSelect = { currentTabState = it }, | ||
| ) | ||
| }, | ||
| ) { it } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HomeNavigation과 FestabookNavigator의 패키지 위치를 지금처럼 설정한 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
navGraph의 경우 각 패키지 별로 설정이 다 다르기 때문에 한 곳에서 모아서 관리하는 것보다, 각 패키지 별로 관리하는 것이 더 가독성이 높다고 판단했습니다.