Bỏ qua để đến nội dung

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à 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

[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" }
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
}
}
}

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)
}
}
}
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")
}
}
}

import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.SlideTransition
@Composable
fun App() {
MaterialTheme {
Navigator(HomeScreen()) { navigator ->
SlideTransition(navigator)
}
}
}
// Slide từ trái/phải
SlideTransition(navigator)
// Fade in/out
FadeTransition(navigator)
// Scale
ScaleTransition(navigator)
// Không có transition
navigator.lastItem.Content()

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)")
}
}
}

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
}
}
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()) }
}

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")
}
}
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) }
)
}

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")
}
}
}
}

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
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ả

Học về ViewModel trong Compose Multiplatform.