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
| 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
| 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ả |
Tiếp theo
Last updated on