Skip to Content

Navigation trong Compose Multiplatform

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ì?

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

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

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

Screen cơ bản

import cafe.adriel.voyager.core.screen.Screen import 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

import cafe.adriel.voyager.core.screen.Screen import kotlinx.serialization.Serializable // Data class phải Serializable @Serializable data 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

App.kt

import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition @Composable fun App() { MaterialTheme { Navigator(HomeScreen()) { navigator -> SlideTransition(navigator) } } }

Các transition có sẵn

// Slide từ trái/phải SlideTransition(navigator) // Fade in/out FadeTransition(navigator) // Scale ScaleTransition(navigator) // Không có transition navigator.lastItem.Content()

Bước 4: Navigation Actions

Trong Screen

import cafe.adriel.voyager.navigator.LocalNavigator import 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)

Tạo ScreenModel

import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import 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

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 module val screenModelModule = module { factory { UserListScreenModel(get()) } }

Bước 6: Tab Navigation

Tạo Tabs

import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions import 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

import cafe.adriel.voyager.navigator.tab.TabNavigator import cafe.adriel.voyager.navigator.tab.CurrentTab @Composable fun MainScreen() { TabNavigator(HomeTab) { tabNavigator -> Scaffold( content = { padding -> Box(modifier = Modifier.padding(padding)) { CurrentTab() } }, bottomBar = { NavigationBar { TabNavigationItem(HomeTab) TabNavigationItem(SearchTab) TabNavigationItem(ProfileTab) } } ) } } @Composable private 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

import cafe.adriel.voyager.navigator.bottomSheet.BottomSheetNavigator import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator @Composable fun 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

ComponentMục đích
ScreenMàn hình/page
NavigatorQuản lý stack
ScreenModelLogic như ViewModel
TabTab navigation
BottomSheetNavigatorBottom sheet
ActionMô 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ả

Tiếp theo

Học về ViewModel trong Compose Multiplatform.

Last updated on