Navigation trong Jetpack Compose lúc mới bắt đầu thì khá đơn giản.
Nhưng khi app lớn dần (multi-module, nhiều flow, nhiều entry point), nếu không thiết kế từ đầu, bạn sẽ rất dễ rơi vào tình trạng:
- Navigation rối
- Khó maintain
- Khó test
Bài viết này đi từ basic đến architecture thực tế, giúp bạn scale navigation đúng cách.
Navigation trong Compose thực chất là gì?
Ở mức cơ bản, Navigation trong Compose gồm:
NavController: quản lý điều hướng và back stackNavHost: container chứa các màn hìnhcomposable(route): định nghĩa destination
Đây là nền tảng chính để điều hướng giữa các màn hình.
Giai đoạn 1 – App nhỏ (simple navigation)
Ở app đơn giản, bạn thường làm kiểu này:
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen() }
composable("detail") { DetailScreen() }
}
Mọi thứ hoạt động tốt, dễ hiểu và dễ debug.
Nhưng…
Vấn đề bắt đầu xuất hiện khi app lớn dần
Khi app scale lên, bạn sẽ gặp:
- Route string hardcode khắp nơi
- Khó truyền dữ liệu phức tạp
- Navigation logic bị rải rác
- Khó reuse screen
Đây là lúc cần chuyển sang architecture rõ ràng hơn.
Giai đoạn 2 – Tách Navigation thành layer riêng
Một nguyên tắc quan trọng:
Navigation không nên nằm trực tiếp trong UI.
Thay vì:
navController.navigate("detail/$id")
Bạn nên:
- Tạo class định nghĩa route
- Centralize navigation logic
Ví dụ:
sealed class Screen(val route: String) {
object Home : Screen("home")
object Detail : Screen("detail/{id}")
}
Lợi ích:
- Type-safe hơn
- Tránh typo
- Dễ maintain
Giai đoạn 3 – Event-based navigation
Một sai lầm phổ biến:
Truyền NavController vào ViewModel
Cách tốt hơn:
ViewModel chỉ emit event:
sealed class UiEvent {
data class Navigate(val route: String) : UiEvent()
}
UI layer sẽ handle:
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is UiEvent.Navigate -> navController.navigate(event.route)
}
}
}
Ưu điểm:
- Tách biệt rõ ràng UI và logic
- Dễ test
- Không leak context
Giai đoạn 4 – Modular navigation (chuẩn production)
Khi app lớn (multi-module):
Mỗi feature nên có:
- Navigation graph riêng
- Route riêng
- Entry point rõ ràng
Ví dụ:
fun NavGraphBuilder.authGraph(navController: NavController) {
composable("login") { LoginScreen() }
composable("register") { RegisterScreen() }
}
Sau đó combine:
NavHost(navController, startDestination = "auth") {
authGraph(navController)
mainGraph(navController)
}
Lợi ích:
- Scale tốt
- Team làm song song được
- Không conflict
Deep Link và navigation nâng cao
Compose Navigation hỗ trợ:
- Deep link từ URL
- Nested navigation graph
- Adaptive navigation UI
Ví dụ:
composable(
route = "profile/{id}",
deepLinks = listOf(
navDeepLink { uriPattern = "https://example.com/profile/{id}" }
)
) { ... }
Cho phép mở app từ link bên ngoài.
Nguyên tắc quan trọng khi scale navigation
- Không hardcode route
- Không để ViewModel giữ NavController
- Tách navigation theo feature
- Luôn nghĩ đến testability
Một insight quan trọng
Navigation không chỉ là UI.
Nó chính là flow của business logic.
Ví dụ:
- User login xong đi đâu
- User chưa onboard thì xử lý thế nào
Đây là logic cấp ứng dụng, không phải UI thuần.
Tổng kết
Scaling navigation trong Compose thường đi qua các bước:
- Basic NavHost
- Tách route và structure
- Event-based navigation
- Modular architecture
Nếu làm đúng từ đầu:
- Code sạch hơn
- Dễ maintain
- Scale tốt hơn
Kết luận
Navigation trong Compose không khó.
Nhưng nếu không thiết kế sớm, nó sẽ nhanh chóng trở thành phần khó kiểm soát nhất trong app.