KMP Tutorial #10: Shared ViewModel in Kotlin Multiplatform

On Android, ViewModels hold UI state and survive configuration changes. On iOS, ObservableObjects do the same job. Two platforms, two patterns, two codebases. But the logic is often identical. Loading a user list, handling errors, managing form state — the business logic does not change between platforms. Only the UI does. With KMP, you can write your ViewModel once in commonMain and use it on both Android and iOS. Since androidx.lifecycle:lifecycle-viewmodel version 2.8, ViewModel officially supports Kotlin Multiplatform. ...

April 4, 2026 · 7 min

Jetpack Compose Tutorial #23: Building the UI Layer — Screens and ViewModels

In the previous tutorial, we built the data layer — Room entities, DAO, repository, and use cases. Now we connect it to the UI. This tutorial builds the three main screens of our task manager app: the task list, the add task form, and the settings screen. What We Build UI Layer: ├── TaskListScreen + TaskListViewModel (MVI) │ ├── Search bar │ ├── Filter chips (All, Active, Completed) │ ├── Task cards with priority badge + category │ └── Swipe to delete ├── AddTaskScreen + AddTaskViewModel │ ├── Title + description fields │ ├── Category chips │ ├── Priority chips │ └── Save button with validation └── SettingsScreen + SettingsViewModel ├── Dark mode toggle └── Delete completed tasks Task List Screen — State and Intents State // ui/screens/tasklist/TaskListState.kt // Everything the task list screen needs to display data class TaskListState( val tasks: List<Task> = emptyList(), val filter: TaskFilter = TaskFilter.ALL, val searchQuery: String = "", val isLoading: Boolean = true, val error: String? = null, val taskCount: Int = 0, val completedCount: Int = 0 ) Intents // ui/screens/tasklist/TaskListIntent.kt // Every action the user can take on the task list sealed interface TaskListIntent { data class Search(val query: String) : TaskListIntent data class ChangeFilter(val filter: TaskFilter) : TaskListIntent data class ToggleTask(val taskId: Long) : TaskListIntent data class DeleteTask(val taskId: Long) : TaskListIntent data object DeleteCompleted : TaskListIntent data object Retry : TaskListIntent } ViewModel // ui/screens/tasklist/TaskListViewModel.kt @HiltViewModel class TaskListViewModel @Inject constructor( private val getTasks: GetTasksUseCase, private val toggleTask: ToggleTaskUseCase, private val deleteTask: DeleteTaskUseCase, private val repository: TaskRepository ) : ViewModel() { private val _state = MutableStateFlow(TaskListState()) val state: StateFlow<TaskListState> = _state.asStateFlow() private var searchJob: Job? = null init { observeTasks() observeCounts() } fun onIntent(intent: TaskListIntent) { when (intent) { is TaskListIntent.Search -> { _state.update { it.copy(searchQuery = intent.query) } observeTasks() } is TaskListIntent.ChangeFilter -> { _state.update { it.copy(filter = intent.filter) } observeTasks() } is TaskListIntent.ToggleTask -> { viewModelScope.launch { toggleTask(intent.taskId) } } is TaskListIntent.DeleteTask -> { viewModelScope.launch { deleteTask(intent.taskId) } } is TaskListIntent.DeleteCompleted -> { viewModelScope.launch { repository.deleteCompletedTasks() } } is TaskListIntent.Retry -> { _state.update { it.copy(error = null) } observeTasks() } } } private fun observeTasks() { searchJob?.cancel() searchJob = viewModelScope.launch { _state.update { it.copy(isLoading = true) } try { getTasks( filter = _state.value.filter, searchQuery = _state.value.searchQuery ).collect { tasks -> _state.update { it.copy(tasks = tasks, isLoading = false, error = null) } } } catch (e: Exception) { _state.update { it.copy(error = e.message, isLoading = false) } } } } private fun observeCounts() { viewModelScope.launch { repository.getTaskCount().collect { count -> _state.update { it.copy(taskCount = count) } } } viewModelScope.launch { repository.getCompletedCount().collect { count -> _state.update { it.copy(completedCount = count) } } } } } The ViewModel pattern: ...

March 26, 2026 · 9 min

Jetpack Compose Tutorial #9: ViewModel — Managing Screen Logic

In Tutorial #5, we learned about remember and mutableStateOf. They work great for simple state. But they have a big problem — when your app needs to load data from a database or API, where does that logic go? Not in the Composable. Composables are for UI, not business logic. That is what ViewModel is for. What is ViewModel? ViewModel is a class that holds your screen’s data and logic. It survives things that destroy and recreate your Composable — like screen rotation or system theme changes. ...

March 22, 2026 · 8 min