Realm's Live Objects: Why Your Query Results Update Themselves
Realm queries return live, auto-updating results instead of static snapshots. Here's how that eliminates UI refresh plumbing, with a Kotlin example and the threading and file-growth caveats that come with it.
22 Jul 2026, 01:17 UTC

If you've built a local-first mobile app with SQLite, you know the drill: write data on a background thread, then figure out how to tell the UI its cached list is stale. Invalidation tokens, Loaders, manual diffing — the plumbing often outweighs the feature. Realm's answer is unusual enough to be worth understanding on its own terms: query results are live. They reflect committed writes automatically, and your UI observes them instead of re-querying.
This post uses the Realm Kotlin SDK (the current multiplatform SDK). The older Realm Java API differs in naming, so don't mix snippets from both.
What "live" actually means
A Realm query doesn't copy rows into a list. It returns a lazily evaluated view over the database file. Two consequences follow:
- Zero-copy reads. Iterating a large result set doesn't materialize every object into memory up front; objects are resolved as you access them.
- Auto-updating results. After any write transaction commits on that Realm, the results collection reflects the new state. Register a change listener — or collect a Flow in the Kotlin SDK — and you get notified with fine-grained change information (insertions, deletions, modifications) suitable for driving a diffing UI.
The practical effect: the "refresh the screen after a write" problem largely disappears. The database pushes changes to whoever is observing.
Worked example: a to-do list with no refresh code
Assume a model and a screen that observes it. On Android with the Kotlin SDK:
class Task : RealmObject {
var title: String = ""
var done: Boolean = false
}
// In a ViewModel — one Realm instance, held for its lifetime
val realm = Realm.open(RealmConfiguration.create(schema = setOf(Task::class)))
val tasks: Flow<ResultsChange<Task>> =
realm.query<Task>("done == false").asFlow()Collecting tasks in the UI layer emits the initial result and then a new emission every time a committed write changes matching rows. A background writer just writes:
// Any thread with its own Realm instance
realm.write {
copyToRealm(Task().apply { title = "Buy coffee" })
}No event bus, no manual re-query, no notifyDataSetChanged(). The collector receives the updated results and the list re-renders. The write { } block is the transaction: writes are ACID and atomic, and everything inside the block commits or rolls back together.
The constraints that come with it
Live objects aren't free magic. Three rules shape how you architect around them:
- Thread confinement. Live objects and Realm instances can't cross threads. The idiomatic pattern is one Realm instance per thread (open it where you use it), or convert data before handing it off — frozen objects in the Kotlin SDK, or
copyFromRealmin the Java SDK. Frozen and copied objects are plain snapshots: they stop updating. Be explicit about which mode a given object is in. - Transactions block other writers. Realm supports many concurrent readers but one writer at a time. Doing slow work — network calls, heavy computation — inside a
writeblock stalls every other writer. Prepare data first, then make the transaction as short as possible. - File growth from version retention. While older versions of the data are pinned (for example, by an open Realm on another thread), the file keeps those versions around and can grow faster than expected. Compaction isn't automatic in every SDK; there's a
compact()API you can call, typically at app startup when no other instance holds the file open.
Trade-off: when live objects aren't what you want
The live-object model shines for UI-bound data on the main thread. It's a poor fit for batch pipelines that ship data across threads or serialize it out of the process — there, you pay the copy cost anyway and a plain ORM or SQLDelight-style flow may be simpler. Also note the ecosystem context: MongoDB deprecated the original Realm Sync in favor of Atlas Device Sync, and sync roadmap details change over time. The local database SDKs remain open source, but verify the current state of sync before committing to it.
How to verify the behavior yourself
Don't take the auto-update claim on faith — it's easy to test:
- Build the to-do example with the current Kotlin SDK. Insert a task from a background thread while the list screen is visible; confirm the UI updates with no re-query in your code.
- Check the SDK changelog for your platform to confirm the
asFlow()/ listener API names still match the release you're using — these have changed between major versions. - Log the Realm file size before and after a loop of several thousand small write transactions to see version-retention growth, then call
compact()and compare.
If your app's core loop is "write locally, reflect it in the UI immediately," Realm's live objects remove a whole category of plumbing. Just respect the threading rules and keep your transactions short.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.