Why Does a Room Flow Update Automatically?

When a Room DAO returns a Flow, the UI can receive new data automatically after the underlying table changes. That does not happen because Flow can detect database changes on its own. It happens because Room connects database invalidation notifications to Flow.

The chain looks like this: when the Flow is collected, Room runs the query; Room observes the relevant tables through InvalidationTracker; a table change triggers invalidation; Room reruns the query and emits the new result to the Flow.

Flow itself does not listen to databases

Kotlin Flow is an asynchronous stream abstraction. It knows how to emit values, cancel work, handle backpressure, and switch contexts. It does not know when a SQLite table changes. The automatic update behavior comes from the DAO implementation generated by Room.

When you write a DAO like this:

@Query("SELECT * FROM user ORDER BY updatedAt DESC")
fun observeUsers(): Flow<List<User>>

Room uses KSP or KAPT at compile time to generate the implementation. That implementation does more than execute SQL once. It binds the query to table observation. When collection starts, it runs the initial query; when a related table changes, it schedules the query again.

What InvalidationTracker does

Room’s InvalidationTracker keeps track of which tables are being observed and when those tables change. For a DAO query, Room can analyze the SQL and identify related tables, such as user, message, or conversation.

After a database transaction commits, if one of those related tables was written to, InvalidationTracker notifies its observers. The Flow does not receive the changed row directly. Instead, Room reruns the entire query and emits the new query result to collectors.

This is important: Room Flow auto-update is “rerun after table invalidation,” not “apply an incremental patch for one row.” The heavier the query and the more frequently the table changes, the more attention you need to pay to requery cost.

Why transactions matter

If one business operation writes multiple tables, put those writes in a transaction. Otherwise, the UI may observe intermediate states: writing the first table triggers one invalidation, writing the second table triggers another, and the list may briefly show inconsistent data.

When the notification is emitted after a single transaction commit, Room can reduce duplicate queries and the UI sees a state closer to business-level consistency.

This is especially important with Paging3 and Room. If a RemoteMediator writes list data and remote keys outside the same transaction, a crash or cancellation can leave the list rows and paging keys inconsistent. Later paging requests can then behave incorrectly.

Flow is cold

DAO Flows are usually cold streams. Without a collector, they do not keep querying the database. With a collector, they register observation and execute the query. Multiple collectors may trigger multiple queries, depending on whether you share the stream upstream with stateIn or shareIn.

In a ViewModel, if several UI components need the same database data, you can consider converting the DAO Flow into a StateFlow:

val users = dao.observeUsers()
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

That way, a screen rotation or brief resubscription does not require every collector to rebuild the same state from scratch. Whether this is worth doing depends on the amount of data and the page structure. Not every DAO Flow must be converted with stateIn.

Common misconceptions about automatic updates

The first misconception is that any database change will trigger the Flow. Room only observes the tables related to the query. If you update an unrelated table, the current Flow will not emit again.

The second misconception is that identical results will not be emitted. Room reruns the query after invalidation. Deduplication depends on whether the upstream uses distinctUntilChanged() and whether entity equality is implemented sensibly.

The third misconception is placing a complex join on top of high-frequency update tables without considering cost. If any related table changes, the whole query may run again. Complex statistics, aggregations, and joins need appropriate indexes and query-cost review.

The fourth misconception is doing heavy result handling on the main thread. Room can run the query itself off the main thread, but after the result is emitted to the UI, a large map transformation can still block the main thread. Expensive transformation should run on an appropriate dispatcher or be modeled earlier.

When manual refresh is still needed

Room Flow solves local database change notification. It does not solve network synchronization. If server data changes, the database will not know unless a sync job fetches the new data and writes it locally.

A common architecture is for the Repository to expose a Room Flow while refresh logic pulls from the network and writes into the database. The UI observes the Flow to get the latest local state; pull-to-refresh, scheduled sync, RemoteMediator, or background work updates the database. This keeps local and remote responsibilities clear, and the UI does not need to care whether data came from the network or cache.

Conclusion

Room Flow auto-update is built from generated Room code, InvalidationTracker, and requerying. Flow provides the asynchronous stream model; Room provides database invalidation notifications. Together, they create reactive database access.

Once you understand that chain, many behaviors make sense: why the UI updates after a database write, why complex queries can execute frequently, why transactions reduce intermediate states, and why network changes do not automatically appear in a Flow.

Further reading