Skip to Content

ViewModel Multiplatform

Hướng dẫn sử dụng ViewModel trong Compose Multiplatform để quản lý state và logic.

Các lựa chọn ViewModel cho KMP

LibraryMô tảPros/Cons
Voyager ScreenModelCủa Voyager✅ Đơn giản, ❌ Gắn với Voyager
MOKO MVVMCủa IceRock✅ Đầy đủ, ⚠️ Setup phức tạp
CustomTự viết✅ Linh hoạt, ⚠️ Phải tự quản lý
AndroidX ViewModelCủa Google✅ Stable, ❌ Android-centric

Cách 1: Voyager ScreenModel (Khuyến nghị)

Setup

// build.gradle.kts commonMain.dependencies { implementation(libs.voyager.screenmodel) implementation(libs.voyager.koin) // Nếu dùng Koin }

Tạo ScreenModel

import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class UserListScreenModel( private val repository: UserRepository ) : ScreenModel { // UI State private val _state = MutableStateFlow<UserListState>(UserListState.Loading) val state: StateFlow<UserListState> = _state.asStateFlow() init { loadUsers() } fun loadUsers() { screenModelScope.launch { _state.value = UserListState.Loading try { val users = repository.getUsers() _state.value = if (users.isEmpty()) { UserListState.Empty } else { UserListState.Success(users) } } catch (e: Exception) { _state.value = UserListState.Error(e.message ?: "Unknown error") } } } fun deleteUser(user: User) { screenModelScope.launch { repository.deleteUser(user.id) loadUsers() // Refresh } } override fun onDispose() { // Cleanup resources nếu cần } } sealed class UserListState { object Loading : UserListState() data class Success(val users: List<User>) : UserListState() data class Error(val message: String) : UserListState() object Empty : UserListState() }

Sử dụng trong Screen

import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.getScreenModel class UserListScreen : Screen { @Composable override fun Content() { val screenModel = getScreenModel<UserListScreenModel>() val state by screenModel.state.collectAsState() val navigator = LocalNavigator.currentOrThrow UserListContent( state = state, onRefresh = { screenModel.loadUsers() }, onUserClick = { user -> navigator.push(UserDetailScreen(user.id)) }, onDeleteUser = { user -> screenModel.deleteUser(user) } ) } }

Cách 2: Custom ViewModel

Base ViewModel

// commonMain/presentation/ViewModel.kt import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext abstract class ViewModel { private val job = SupervisorJob() protected val viewModelScope = CoroutineScope(job + Dispatchers.Main) open fun onCleared() { job.cancel() } }

Implement ViewModel

class HomeViewModel( private val userRepository: UserRepository, private val postRepository: PostRepository ) : ViewModel() { private val _uiState = MutableStateFlow(HomeUiState()) val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow() init { loadData() } fun loadData() { viewModelScope.launch { _uiState.update { it.copy(isLoading = true) } try { val users = async { userRepository.getTopUsers(5) } val posts = async { postRepository.getLatestPosts(10) } _uiState.update { it.copy( isLoading = false, topUsers = users.await(), latestPosts = posts.await() ) } } catch (e: Exception) { _uiState.update { it.copy(isLoading = false, error = e.message) } } } } } data class HomeUiState( val isLoading: Boolean = false, val topUsers: List<User> = emptyList(), val latestPosts: List<Post> = emptyList(), val error: String? = null )

Composable Extension

@Composable fun <T : ViewModel> rememberViewModel(factory: () -> T): T { return remember { factory() }.also { viewModel -> DisposableEffect(viewModel) { onDispose { viewModel.onCleared() } } } } // Sử dụng @Composable fun HomeScreen() { val viewModel = rememberViewModel { HomeViewModel(get(), get()) } val state by viewModel.uiState.collectAsState() // ... }

Cách 3: MOKO MVVM

Setup

// build.gradle.kts commonMain.dependencies { implementation("dev.icerock.moko:mvvm-core:0.16.1") implementation("dev.icerock.moko:mvvm-compose:0.16.1") } androidMain.dependencies { implementation("dev.icerock.moko:mvvm-flow:0.16.1") } iosMain.dependencies { implementation("dev.icerock.moko:mvvm-flow:0.16.1") }

Tạo ViewModel

import dev.icerock.moko.mvvm.viewmodel.ViewModel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class ProductViewModel( private val repository: ProductRepository ) : ViewModel() { private val _products = MutableStateFlow<List<Product>>(emptyList()) val products: StateFlow<List<Product>> = _products.asStateFlow() private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow() fun loadProducts() { viewModelScope.launch { _isLoading.value = true _products.value = repository.getProducts() _isLoading.value = false } } }

Sử dụng

import dev.icerock.moko.mvvm.compose.getViewModel import dev.icerock.moko.mvvm.compose.viewModelFactory @Composable fun ProductScreen() { val viewModel = getViewModel( key = "product-vm", factory = viewModelFactory { ProductViewModel(get()) } ) val products by viewModel.products.collectAsState() val isLoading by viewModel.isLoading.collectAsState() // UI... }

State Management Patterns

Single State vs Multiple Flows

// Pattern 1: Single UI State (Khuyến nghị) data class UserUiState( val isLoading: Boolean = false, val users: List<User> = emptyList(), val error: String? = null, val selectedUser: User? = null ) class UserViewModel : ScreenModel { private val _uiState = MutableStateFlow(UserUiState()) val uiState: StateFlow<UserUiState> = _uiState.asStateFlow() fun selectUser(user: User) { _uiState.update { it.copy(selectedUser = user) } } } // Pattern 2: Multiple Flows (Cho cases đơn giản) class SimpleViewModel : ScreenModel { private val _users = MutableStateFlow<List<User>>(emptyList()) val users: StateFlow<List<User>> = _users private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow<Boolean> = _isLoading }

One-time Events

class AuthViewModel : ScreenModel { private val _uiState = MutableStateFlow(AuthUiState()) val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow() // SharedFlow cho one-time events private val _events = MutableSharedFlow<AuthEvent>() val events: SharedFlow<AuthEvent> = _events.asSharedFlow() fun login(email: String, password: String) { screenModelScope.launch { _uiState.update { it.copy(isLoading = true) } try { authRepository.login(email, password) _events.emit(AuthEvent.NavigateToHome) } catch (e: Exception) { _events.emit(AuthEvent.ShowError(e.message ?: "Login failed")) } finally { _uiState.update { it.copy(isLoading = false) } } } } } sealed class AuthEvent { object NavigateToHome : AuthEvent() data class ShowError(val message: String) : AuthEvent() } // Collect events trong Screen @Composable fun LoginScreen() { val viewModel = getScreenModel<AuthViewModel>() val navigator = LocalNavigator.currentOrThrow LaunchedEffect(Unit) { viewModel.events.collect { event -> when (event) { is AuthEvent.NavigateToHome -> navigator.replaceAll(HomeScreen()) is AuthEvent.ShowError -> { // Show snackbar } } } } // UI... }

Koin Integration

// di/ViewModelModule.kt val viewModelModule = module { // Voyager ScreenModels factory { UserListScreenModel(get()) } factory { UserDetailScreenModel(get()) } factory { (userId: Long) -> UserEditScreenModel(userId, get()) } // HomeScreenModel với nhiều dependencies factory { HomeScreenModel(get(), get(), get()) } } // Sử dụng với parameters class UserEditScreen(private val userId: Long) : Screen { @Composable override fun Content() { val screenModel = getScreenModel<UserEditScreenModel> { parametersOf(userId) } // ... } }

📝 Tóm tắt

ApproachKhi nào dùng
Voyager ScreenModelDùng Voyager cho navigation
Custom ViewModelCần flexibility cao
MOKO MVVMCần full-featured MVVM

Best Practices

  1. Single source of truth - một StateFlow cho UI state
  2. Unidirectional data flow - State down, Events up
  3. SharedFlow cho one-time events (navigation, snackbar)
  4. Inject via DI - Không tạo trực tiếp trong Composable

Tiếp theo

Học về Tích hợp với iOS.

Last updated on