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

Rust Tutorial #18: Testing in Rust

In the previous tutorial, we learned modules and project organization. Now we learn testing – one of Rust’s best features. Rust has testing built into the language and toolchain. You do not need to install a separate testing framework. Write #[test], run cargo test, and you are done. The compiler and test runner handle everything. Good tests give you confidence to refactor code, add features, and fix bugs without breaking existing functionality. ...

March 26, 2026 · 9 min

Rust Tutorial #17: Modules and Crates

In the previous tutorial, we learned Rust collections. As projects grow, putting everything in one file becomes unmanageable. In this tutorial, we learn modules – Rust’s system for organizing code into logical units. Good project structure makes code easier to read, test, and maintain. Rust’s module system is simple once you understand the rules. Let us learn them step by step. What is a Module? A module is a named container for functions, structs, enums, and other items. Modules control: ...

March 26, 2026 · 8 min

Rust Tutorial #15: Async/Await and Tokio

In the previous tutorial, we learned threads, channels, and Mutex for concurrency. Now we learn async/await – a different way to handle concurrent work. Threads are good when you have CPU-heavy tasks. But many programs spend most of their time waiting – for network responses, file reads, or database queries. Creating one thread per request wastes memory. Async programming solves this problem. It lets one thread handle thousands of waiting tasks. ...

March 26, 2026 · 8 min

Rust Tutorial #16: Channels and Message Passing

In the previous tutorial, we learned async programming with Tokio. Now we dive deeper into channels — the primary way async tasks communicate with each other. Channels let tasks send and receive messages without sharing memory directly. This is the “message passing” model of concurrency. Instead of locking shared data with a Mutex, you send data through a channel. The task that receives it owns it completely. Tokio provides four channel types. Each one solves a different problem. By the end of this tutorial, you will know when to use each one. ...

March 26, 2026 · 10 min

Rust Tutorial #14: Concurrency — Threads, Channels, and Message Passing

In the previous tutorial, we learned smart pointers. Now we learn concurrency — running code on multiple threads at the same time. Concurrency is hard in most languages. Data races, deadlocks, and race conditions cause bugs that only show up in production. Rust prevents most of these bugs at compile time. The ownership system guarantees that you cannot share data unsafely between threads. This is called fearless concurrency. The compiler catches mistakes before your code runs. ...

March 26, 2026 · 8 min

Rust Tutorial #13: Smart Pointers — Box, Rc, Arc

In the previous tutorial, we learned closures and iterators. Now we learn smart pointers — types that act like pointers but have extra capabilities. In Rust, the most common pointer is a reference (&T). References borrow data but do not own it. Smart pointers own the data they point to. They also add features like heap allocation, reference counting, and interior mutability. What Is a Smart Pointer? A smart pointer is a struct that: ...

March 26, 2026 · 9 min

Rust Tutorial #16: Collections — HashMap, BTreeMap, VecDeque

In the previous tutorial, we learned async programming with Tokio. Now we take a deep dive into Rust collections – HashMap, BTreeMap, HashSet, BTreeSet, VecDeque, and BinaryHeap. You already know Vec and basic HashMap. This tutorial covers the advanced features: the entry API, custom keys, range queries, set operations, sliding windows, and priority queues. HashMap – The Entry API The entry API is the most important HashMap feature to learn. It lets you insert or update values without looking up the key twice. ...

March 26, 2026 · 8 min

Rust Tutorial #12: Closures and Iterators

In the previous tutorial, we learned lifetimes. Now we learn two features that make Rust code clean and expressive: closures and iterators. Closures are anonymous functions you can store in variables and pass to other functions. Iterators let you process collections step by step. Together, they replace most loops with short, readable code. What Is a Closure? A closure is a function without a name. It can capture variables from its environment: ...

March 26, 2026 · 9 min

Rust Tutorial #11: Lifetimes — How Rust Prevents Dangling References

In the previous tutorial, we learned generics. Now we learn lifetimes — the last piece of Rust’s ownership system. Lifetimes answer one question: “How long does this reference live?” The Rust compiler uses lifetimes to make sure every reference is always valid. No dangling pointers. No use-after-free bugs. No segfaults. If you have struggled with lifetime errors, this tutorial will help. Most of the time, lifetimes are invisible — the compiler figures them out for you. But sometimes you need to tell the compiler what you mean. ...

March 26, 2026 · 9 min