-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
66: Building a Task Manager with Coroutines and Flow
I want to start today with a piece of code that looks perfectly reasonable at first glance. Imagine you're building the core logic for a Task Manager. You want a way to stream the current list of tasks to the UI so that whenever a task is added or completed, the screen updates automatically. You might write something like this:
class TaskManager {
private val tasks = mutableListOf<Task>()
fun getTasks(): Flow<List<Task>> = flow {
emit(tasks.toList())
}
suspend fun addTask(task: Task) {
tasks.add(task)
}
}
The "Ghost Update" Bug
If you run this, you'll notice something frustrating: you call addTask(), the list inside the manager grows, but your UI doesn't budge. You're collecting the flow, but the updates never arrive. Why?
The issue here is that you've created a cold flow. In Kotlin, the flow { ... } builder creates a stream that only executes its code when it is collected. Every time you call getTasks().collect(), it simply emits the current state of the list and then completes. It has no mechanism to "push" new data when the tasks list changes. It's essentially a fancy way of returning a list once, rather than a living stream of data.
Switching to StateFlow for Persistent State
To fix this, we need a hot flow. Specifically, we want a StateFlow. Unlike a cold flow, a StateFlow exists independently of collectors; it holds a value in memory and broadcasts that value to anyone listening.
Here is how I would refactor this to make it actually work in a production environment:
class TaskManager {
// We keep the mutable state private so external classes can't mess with it
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
// We expose a read-only StateFlow to the outside world
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
suspend fun addTask(task: Task) {
// Use .update { } to ensure thread-safe atomic updates
_tasks.update { currentTasks ->
currentTasks + task
}
}
suspend fun toggleTaskCompletion(taskId: Int) {
_tasks.update { currentTasks ->
currentTasks.map {
if (it.id == taskId) it.copy(isCompleted = !it.isCompleted) else it
}
}
}
}
Notice the use of _tasks.update { ... }. I've seen a lot of developers just do _tasks.value = _tasks.value + task. In a multi-threaded environment, that's a recipe for race conditions. update ensures that you're working with the most recent state before applying your change.
Combining Multiple Streams for Filtering
A Task Manager isn't very useful if you can't filter your tasks. This is where the real power of Flow comes in. Instead of creating a separate function for "get completed tasks," we can treat the filter itself as a stream of data and combine it with our task list.
I like to think of this as a "pipeline." We have a stream of tasks and a stream of filter preferences. Whenever either one changes, the pipeline recalculates the result automatically.
class TaskViewModel(private val taskManager: TaskManager) {
private val filterState = MutableStateFlow(Filter.ALL)
// Combine the tasks flow and the filter flow into one result stream
val filteredTasks: Flow<List<Task>> = combine(
taskManager.tasks,
filterState
) { tasks, filter ->
when (filter) {
Filter.ALL -> tasks
Filter.COMPLETED -> tasks.filter { it.isCompleted }
Filter.ACTIVE -> tasks.filter { !it.isCompleted }
}
}
fun setFilter(filter: Filter) {
filterState.value = filter
}
}
By using combine, you've removed the need to manually call a "refresh" function. If a task is marked as completed while the user is looking at the "Active" filter, that task will instantly vanish from the list because the filteredTasks flow re-emits the filtered result the moment taskManager.tasks updates.
📋 Practical Task
Exercise: Implementing a Real-Time Search Query Filter
You need to extend the current Task Manager logic to support a real-time search bar. Instead of just filtering by status (All/Active/Completed), the user should be able to type a query that filters tasks by their title.
Your Task:
- Create a
MutableStateFlow<String>to hold the current search query. - Modify the
filteredTasksflow (or create a new one) using thecombineoperator to filter the task list by both the currentFilterstatus AND the search query string. - Ensure the search is case-insensitive.
- Implement a
updateSearchQuery(query: String)function that updates the state flow.
Hint: You will be combining three different flows: the task list, the status filter, and the search query string.
There are no comments for now.