Navigation
Hướng dẫn setup navigation cho ứng dụng Compose Multiplatform sử dụng Voyager - thư viện navigation phổ biến nhất cho CMP.
Voyager là gì?
Phần tiêu đề “Voyager là gì?”Voyager là thư viện navigation cho Compose Multiplatform, cung cấp:
- Type-safe navigation
- Screen models (như ViewModel)
- Tab navigation
- Bottom sheet navigation
- Nested navigation
- Transitions
Bước 1: Thêm Dependencies
Phần tiêu đề “Bước 1: Thêm Dependencies”libs.versions.toml
Phần tiêu đề “libs.versions.toml”[versions]voyager = "1.0.0"
[libraries]voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }shared/build.gradle.kts
Phần tiêu đề “shared/build.gradle.kts”kotlin { sourceSets { commonMain.dependencies { implementation(libs.voyager.navigator) implementation(libs.voyager.screenmodel) implementation(libs.voyager.tab.navigator) implementation(libs.voyager.transitions) implementation(libs.voyager.koin) // Nếu dùng Koin } }}Bước 2: Tạo Screens
Phần tiêu đề “Bước 2: Tạo Screens”Screen cơ bản
Phần tiêu đề “Screen cơ bản”import cafe.adriel.voyager.core.screen.Screenimport androidx.compose.runtime.Composable
class HomeScreen : Screen {
@Composable override fun Content() { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { Text("Home Screen", style = MaterialTheme.typography.headlineLarge) } }}Screen với data
Phần tiêu đề “Screen với data”import cafe.adriel.voyager.core.screen.Screenimport kotlinx.serialization.Serializable
// Data class phải Serializable@Serializabledata class UserDetailScreen( val userId: Long, val userName: String) : Screen {
@Composable override fun Content() { Column(modifier = Modifier.padding(16.dp)) { Text("User ID: $userId") Text("User Name: $userName") } }}Bước 3: Setup Navigator
Phần tiêu đề “Bước 3: Setup Navigator”App.kt
Phần tiêu đề “App.kt”import cafe.adriel.voyager.navigator.Navigatorimport cafe.adriel.voyager.transitions.SlideTransition
@Composablefun App() { MaterialTheme { Navigator(HomeScreen()) { navigator -> SlideTransition(navigator) } }}Các transition có sẵn
Phần tiêu đề “Các transition có sẵn”// Slide từ trái/phảiSlideTransition(navigator)
// Fade in/outFadeTransition(navigator)
// ScaleScaleTransition(navigator)
// Không có transitionnavigator.lastItem.Content()Bước 4: Navigation Actions
Phần tiêu đề “Bước 4: Navigation Actions”Trong Screen
Phần tiêu đề “Trong Screen”import cafe.adriel.voyager.navigator.LocalNavigatorimport cafe.adriel.voyager.navigator.currentOrThrow
class HomeScreen : Screen {
@Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow
Column { Text("Home Screen")
// Push screen mới Button(onClick = { navigator.push(UserListScreen()) }) { Text("Go to User List") }
// Push với data Button(onClick = { navigator.push(UserDetailScreen(1, "John")) }) { Text("Go to User Detail") }
// Replace screen hiện tại Button(onClick = { navigator.replace(SettingsScreen()) }) { Text("Replace with Settings") }
// Replace tất cả với screen mới Button(onClick = { navigator.replaceAll(LoginScreen()) }) { Text("Logout") } } }}
class UserDetailScreen(val userId: Long, val userName: String) : Screen {
@Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow
Column { // Back button IconButton(onClick = { navigator.pop() }) { Icon(Icons.Default.ArrowBack, "Back") }
Text("User: $userName (ID: $userId)") } }}Bước 5: ScreenModel (ViewModel)
Phần tiêu đề “Bước 5: ScreenModel (ViewModel)”Tạo ScreenModel
Phần tiêu đề “Tạo ScreenModel”import cafe.adriel.voyager.core.model.ScreenModelimport cafe.adriel.voyager.core.model.screenModelScopeimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.launch
class UserListScreenModel( private val repository: UserRepository) : ScreenModel {
private val _users = MutableStateFlow<List<User>>(emptyList()) val users: StateFlow<List<User>> = _users
private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow<Boolean> = _isLoading
init { loadUsers() }
fun loadUsers() { screenModelScope.launch { _isLoading.value = true try { _users.value = repository.getUsers() } catch (e: Exception) { // Handle error } finally { _isLoading.value = false } } }
override fun onDispose() { // Cleanup if needed }}Sử dụng với Koin
Phần tiêu đề “Sử dụng với Koin”import cafe.adriel.voyager.koin.getScreenModel
class UserListScreen : Screen {
@Composable override fun Content() { val screenModel = getScreenModel<UserListScreenModel>() val users by screenModel.users.collectAsState() val isLoading by screenModel.isLoading.collectAsState()
if (isLoading) { CircularProgressIndicator() } else { LazyColumn { items(users) { user -> UserItem(user) } } } }}
// Koin moduleval screenModelModule = module { factory { UserListScreenModel(get()) }}Bước 6: Tab Navigation
Phần tiêu đề “Bước 6: Tab Navigation”Tạo Tabs
Phần tiêu đề “Tạo Tabs”import cafe.adriel.voyager.navigator.tab.Tabimport cafe.adriel.voyager.navigator.tab.TabOptionsimport androidx.compose.runtime.remember
object HomeTab : Tab { override val options: TabOptions @Composable get() { val icon = rememberVectorPainter(Icons.Default.Home) return remember { TabOptions( index = 0u, title = "Home", icon = icon ) } }
@Composable override fun Content() { Text("Home Tab Content") }}
object SearchTab : Tab { override val options: TabOptions @Composable get() { val icon = rememberVectorPainter(Icons.Default.Search) return remember { TabOptions( index = 1u, title = "Search", icon = icon ) } }
@Composable override fun Content() { Text("Search Tab Content") }}
object ProfileTab : Tab { override val options: TabOptions @Composable get() { val icon = rememberVectorPainter(Icons.Default.Person) return remember { TabOptions( index = 2u, title = "Profile", icon = icon ) } }
@Composable override fun Content() { Text("Profile Tab Content") }}TabNavigator
Phần tiêu đề “TabNavigator”import cafe.adriel.voyager.navigator.tab.TabNavigatorimport cafe.adriel.voyager.navigator.tab.CurrentTab
@Composablefun MainScreen() { TabNavigator(HomeTab) { tabNavigator -> Scaffold( content = { padding -> Box(modifier = Modifier.padding(padding)) { CurrentTab() } }, bottomBar = { NavigationBar { TabNavigationItem(HomeTab) TabNavigationItem(SearchTab) TabNavigationItem(ProfileTab) } } ) }}
@Composableprivate fun RowScope.TabNavigationItem(tab: Tab) { val tabNavigator = LocalTabNavigator.current
NavigationBarItem( selected = tabNavigator.current == tab, onClick = { tabNavigator.current = tab }, icon = { tab.options.icon?.let { Icon(it, tab.options.title) } }, label = { Text(tab.options.title) } )}Bước 7: Bottom Sheet Navigation
Phần tiêu đề “Bước 7: Bottom Sheet Navigation”import cafe.adriel.voyager.navigator.bottomSheet.BottomSheetNavigatorimport cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
@Composablefun App() { BottomSheetNavigator { Navigator(HomeScreen()) }}
class HomeScreen : Screen { @Composable override fun Content() { val bottomSheetNavigator = LocalBottomSheetNavigator.current
Button(onClick = { bottomSheetNavigator.show(FilterBottomSheet()) }) { Text("Show Filters") } }}
class FilterBottomSheet : Screen { @Composable override fun Content() { val bottomSheetNavigator = LocalBottomSheetNavigator.current
Column(modifier = Modifier.padding(16.dp)) { Text("Filters", style = MaterialTheme.typography.headlineSmall) // Filter options... Button(onClick = { bottomSheetNavigator.hide() }) { Text("Apply") } } }}📝 Tóm tắt
Phần tiêu đề “📝 Tóm tắt”| Component | Mục đích |
|---|---|
Screen |
Màn hình/page |
Navigator |
Quản lý stack |
ScreenModel |
Logic như ViewModel |
Tab |
Tab navigation |
BottomSheetNavigator |
Bottom sheet |
Navigation Actions
Phần tiêu đề “Navigation Actions”| Action | Mô tả |
|---|---|
push(screen) |
Thêm screen mới |
pop() |
Quay lại |
popAll() |
Về root |
replace(screen) |
Thay thế current |
replaceAll(screen) |
Thay thế tất cả |