Compose에서 화면이 많아질수록
NavHost 안에 composable {} 가 비대해지고,
Navigation 책임이 한 곳에 몰리는 문제가 생깁니다.
일반적인 Compose Navigation은 보통 이렇게 시작합니다.
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen() }
composable("detail") { DetailScreen() }
composable("setting") { SettingScreen() }
}
하지만 화면이 늘어나면
1. NavHost가 비대해지며
2. feature 단위 분리가 어렵고
3. 테스트, 유지보수 난이도 증가합니다.
각 Feature가 자신의 Navigation을 직접 등록한다
이를 위해 공통 인터페이스를 하나 정의합니다.
interface NavigationDestination {
val route: String
fun register(
builder: NavGraphBuilder,
navController: NavHostController
)
}
각 feature는 자신의 route, 자신의 composable 등록 책임만 가지게 됩니다.
Route 문자열을 직접 쓰는 대신
sealed class로 타입 안전한 Route를 만듭니다.
sealed class MiruniRoute(val route: String) {
data object Home : MiruniRoute("home")
data object HomeDndTimerSetting :
MiruniRoute("home/dnd/timer")
data object HomeDndPause :
MiruniRoute("home/dnd/pause")
data object HomeDndComplete :
MiruniRoute("home/dnd/complete/{hour}/{minute}") {
fun createRoute(hour: Int, minute: Int): String {
return "home/dnd/complete/$hour/$minute"
}
}
}
class HomeNavigation @Inject constructor() : NavigationDestination {
override val route: String = MiruniRoute.Home.route
override fun register(
builder: NavGraphBuilder,
navController: NavHostController
) {
builder.composable(MiruniRoute.Home.route) {
HomeScreen(navController)
}
builder.composable(MiruniRoute.HomeDndTimerSetting.route) {
DndTimerScreen(navController)
}
builder.composable(MiruniRoute.HomeDndPause.route) {
DndPauseScreen(navController)
}
}
}
Home 관련 화면은 HomeNavigation이 전담합니다.
Navigation들을 Set으로 주입받습니다.
@Module
@InstallIn(SingletonComponent::class)
abstract class NavigationModule {
@Binds
@IntoSet
abstract fun bindHomeNavigation(
navigation: HomeNavigation
): NavigationDestination
}
이제 App 레벨에서는
@Composable
fun AppNavHost(
navController: NavHostController,
destinations: Set<NavigationDestination>
) {
NavHost(
navController = navController,
startDestination = MiruniRoute.Home.route
) {
destinations.forEach {
it.register(this, navController)
}
}
}
Feature가 추가되어도
NavHost 수정 필요하지 않습니다.
MVI 에서 Navigation 은 Effect 로 정의합니다.
sealed interface DndContract {
sealed interface Effect {
data object NavigateToPause : Effect
data object TimeFinished : Effect
}
}
ViewModel 에서 Effect 발생
setEffect { DndContract.Effect.NavigateToPause }
Composable 에서 Effect 수신 -> Navigate 실행
LaunchedEffect(Unit) {
viewModel.effect.collect { effect ->
when (effect) {
DndContract.Effect.NavigateToPause -> {
navController.navigate(MiruniRoute.HomeDndPause.route)
}
DndContract.Effect.TimeFinished -> {
navController.navigate(
MiruniRoute.HomeDndComplete.createRoute(
state.hours,
state.minutes
)
)
}
}
}
}
ViewModel 은 이동 명령만 내리며
실제 Navigation 실행은 UI 에 있다.
ViewModel
├─ State (화면 상태)
└─ Effect (이동 / 토스트 / 다이얼로그)
Composable
├─ State → UI 렌더링
└─ Effect → Navigation 실행
Navigation
├─ sealed class Route
├─ Feature별 NavigationDestination
└─ DI로 자동 등록