KMP Tutorial #17: Building the UI Layer — Compose + SwiftUI with Shared ViewModel

In the previous tutorial, we built the shared data layer with Ktor, SQLDelight, and offline-first sync. The data layer works, but the UI is basic. You can only see a list of notes with a “+” button that creates placeholder notes. In this tutorial, we build a proper UI layer. You will add navigation between screens, a note editing screen with a color picker, pull-to-refresh sync, and loading and error states. All of this works on both Android (Compose) and iOS (SwiftUI). ...

April 6, 2026 · 14 min

KMP Tutorial #4: Compose Multiplatform — Share Your UI Across Android, iOS, and Desktop

In the previous tutorials, we shared business logic between Android and iOS. The UI stayed separate — Compose on Android, SwiftUI on iOS. But what if you could share the UI too? That is what Compose Multiplatform (CMP) does. Write your Compose code once. Run it on Android, iOS, Desktop, and Web. Same @Composable functions. Same Modifier chains. Same MaterialTheme. If you followed our Jetpack Compose tutorial series, you already know how to build Compose UI. CMP uses the exact same API. No new framework to learn. ...

April 2, 2026 · 9 min

Jetpack Compose Tutorial #24: Navigation, Animations, and Polish

The task manager works. You can add tasks, complete them, delete them, search, and filter. But it doesn’t feel polished. Screens change instantly. Deleting a task is jarring. There’s no feedback when you complete something. This tutorial adds the polish that makes the difference between a homework project and a real app. What We Add Feature What It Does Navigation transitions Screens slide in/out smoothly Swipe to delete Swipe a task left to delete it Animated task completion Checkbox animates, strikethrough fades in Animated list changes Tasks slide in/out when added or removed Empty state animations Gentle fade-in when list is empty Dark mode Follows system theme Snackbar with undo “Task deleted” with undo option Navigation Transitions By default, screens appear instantly. Add slide transitions: ...

March 26, 2026 · 5 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 #15: Animations — Make Your UI Feel Alive

Your app works. But it feels flat. Buttons appear instantly. Screens switch without transition. Content pops in and out like a slideshow from 2005. Animations fix that. They make your app feel smooth, polished, and alive — like a well-made product instead of a homework assignment. The good news: Compose makes animations surprisingly easy. You can add most animations with a single line of code. The Animation API at a Glance Compose has several animation APIs, each for a different use case: ...

March 24, 2026 · 9 min