Skip to content
All posts
May 17, 20265 min read

Buildinga Reactive Data Layer with Android Room and Kotlin Flow

Learn how to replace LiveData with Kotlin Flow in a Room‑based architecture, see real code, and make your Android app truly reactive.

AndroidKotlin
Share:

Android apps are only as fast as their data layer. If you’re still using LiveData or manual threading, you’re leaving performance on the table.

Context
For the past decade I’ve shipped 22+ apps from Bangkok, and I’ve seen the same pattern repeat: a Room database built for CRUD, a ViewModel exposing LiveData, and UI components that observe that LiveData. The result is tight coupling, hard‑to‑test code, and UI jitter when data changes. Kotlin Flow gives us a clean, cold‑stream approach that pairs naturally with Room’s suspend APIs. In this post I’ll show you how to wire Room directly into a Flow‑based repository, expose it from a ViewModel, and consume it in Jetpack Compose without any boilerplate. You’ll walk away with a production‑ready reactive data layer you can drop into any Android project today.

Why Reactive Data Layers Matter

Traditional Android architectures rely on LiveData or RxJava to push UI updates. While functional, they introduce several pain points:

AspectLiveDataKotlin Flow
Cold vs HotHot (emits immediately)Cold (emits on demand)
Lifecycle awarenessAutomatic (LifecycleObserver)Requires explicit collection handling
Error handlingSeparate try/catch neededPropagates as a Throwable in the flow
TestingRequires UI thread setupCan be collected in test coroutines
BackpressureLimitedBuilt‑in via flow operators

[!TIP] Use Flow when you need fine‑grained control over data emission and want to avoid unnecessary recompositions.

[!NOTE] Flow works best when the underlying data source is already suspend‑capable, which is exactly what Room provides.

The real benefit appears when you combine Room’s

code
@Query
suspend functions with Flow. Instead of observing a static LiveData list, you get a stream that emits only when the underlying query actually changes. This eliminates “stale UI” bugs and lets you compose sophisticated data transformations (e.g., combine multiple flows, apply caching, debounce) with minimal code.

Integrating Room with Kotlin Flow

First, define a DAO that returns a Flow. Room can turn any

code
suspend
query into a Flow of
code
List<Entity>
.

kotlin
@Dao
interface UserDao {
    @Query("SELECT * FROM user")
    fun getAllUsers(): Flow<List<User>>
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User)
    
    @Delete
    suspend fun delete(user: User)
}

Next, create a Repository that exposes the DAO’s Flow as the single source of truth.

kotlin
class UserRepository(private val dao: UserDao) {
    // Public Flow that UI will collect
    val allUsers: Flow<List<User>> = dao.getAllUsers()
        .flowOn(Dispatchers.IO)          // Ensure DB work runs off the main thread
        .catch { throw it }               // Propagate errors downstream
}

Now, the ViewModel simply exposes the repository’s Flow as

code
StateFlow
. This conversion guarantees that the UI receives a state that updates automatically when the flow emits.

kotlin
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repository: UserRepository
) : ViewModel() {

    // Transform the cold Flow into a hot StateFlow
    val users: StateFlow<List<User>> = repository.allUsers
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}

[!IMPORTANT]

code
stateIn
is the key operator that turns a Flow into a StateFlow while respecting the ViewModel’s lifecycle.

Persisting and Updating Data

Because the DAO methods are

code
suspend
, you can call them from coroutines without blocking the main thread. Inserting a user is as simple as:

kotlin
fun addUser(name: String, age: Int) {
    viewModelScope.launch {
        repository.insert(User(name = name, age = age))
    }
}

If you need to combine streams — say, fetch users and also listen to a sync flag — use

code
zip
or
code
combine
:

kotlin
val usersWithSync: Flow<List<User>> = repository.allUsers
    .combine(latestSyncFlag) { users, sync -> /* combine logic */ }

[!WARNING] Never collect a Flow on the main thread; always use

code
Dispatchers.IO
or
code
viewModelScope
to keep UI responsive.

Consuming Flow in Compose UI

Jetpack Compose loves StateFlow. The UI layer simply collects the flow as state, which triggers recomposition only when the list actually changes.

kotlin
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val users by viewModel.users.collectAsState()

    LazyColumn {
        items(users) { user ->
            UserRow(user = user)
        }
    }
}

If you want to react to a single item update, you can expose a separate Flow from the repository (e.g.,

code
Flow<User>
) and collect it in the composable. The key is to keep the UI layer declarative and reactive.

[!NOTE]

code
collectAsState()
automatically manages the coroutine scope and cancels collection when the composable leaves the composition, preventing leaks.

Real‑World Example: Syncing with a Remote API

A common pattern is to keep a local cache (Room) and periodically refresh it from a network layer. Here’s a concise example:

kotlin
class SyncRepository(
    private val dao: UserDao,
    private val api: UserApi
) {
    val freshUsers: Flow<List<User>> = flow {
        // 1️⃣ Refresh from network
        val remote = api.getUsers()
        // 2️⃣ Insert into Room (suspend)
        dao.insertAll(remote.toEntityList())
        // 3️⃣ Emit the updated list
        emit(dao.getAllUsers())
    }
        .flowOn(Dispatchers.IO)
        .catch { /* handle network errors */ }
}

The UI can observe

code
freshUsers
just like any other Flow, ensuring the displayed list stays up‑to‑date with minimal code.

Testing the Flow Layer

Testing Flow‑based code is straightforward with the

code
runTest
coroutine builder.

kotlin
@Test
fun `repository emits updated list after insert`() = runTest {
    // Given
    val dao = mockk<UserDao>()
    val repo = UserRepository(dao)
    val mutableList = mutableListOf<User>()
    coEvery { dao.getAllUsers() } returns flowOf(mutableList)

    // When
    repo.insert(User(name = "Alice", age = 30))

    // Then
    val emitted = advanceUntilIdle()
    assertThat(emitted).hasSize(1)
    assertThat(emitted.first()).containsExactly(User(name = "Alice", age = 30))
}

[!TIP] Use

code
advanceUntilIdle()
to let all pending flow emissions complete before asserting.

Key Takeaways

  • Leverage Flow for cold, lifecycle‑aware streams – it eliminates the need for manual observer management.
  • Convert Flow to StateFlow with
    code
    stateIn
    to expose a UI‑friendly observable state from a Repository.
  • Collect Flow in Compose via
    code
    collectAsState()
    – the UI updates only when the underlying data actually changes.
  • Keep DB operations on
    code
    Dispatchers.IO
    and use
    code
    viewModelScope
    for coroutine execution to maintain responsiveness.
  • Test Flow logic with
    code
    runTest
    – the same tools you use for regular coroutines work seamlessly with Room’s Flow.

By adopting Kotlin Flow alongside Android Room, you gain a reactive, testable, and performant data layer that scales with the complexity of modern Android applications. Start integrating Flow today, and watch your apps become faster, more reliable, and easier to maintain.

Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this