Reactive Flows
Reactive Flows
Use the generated flow-based APIs to keep your UI synchronized with database changes.
SQLiteNow tracks table invalidations and notifies observers automatically. Combine those signals with Kotlin Flows to propagate updates through your UI layers.
Basic reactive query
@Composable
fun PersonList() {
var persons by remember { mutableStateOf<List<PersonEntity>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
database.person
.selectAll(PersonQuery.SelectAll.Params(limit = -1, offset = 0))
.asFlow() // Reactive flow
.collect { personList ->
persons = personList
isLoading = false
}
}
LazyColumn {
items(persons) { person ->
PersonCard(person = person)
}
}
}
With data transformation
Perform transformations off the main thread before updating the UI:
LaunchedEffect(Unit) {
database.person
.selectAll(PersonQuery.SelectAll.Params(limit = -1, offset = 0))
.asFlow()
.map { personList ->
personList.map { person ->
person.copy(
displayName = "${person.firstName} ${person.lastName}".trim(),
isRecent = person.createdAt > recentThreshold
)
}
}
.collect { transformedPersons ->
persons = transformedPersons
}
}
Related data updates
Flows can be collected independently for related tables:
@Composable
fun PersonWithComments(personId: ByteArray) {
var person by remember { mutableStateOf<PersonEntity?>(null) }
var comments by remember { mutableStateOf<List<CommentEntity>>(emptyList()) }
LaunchedEffect(personId.toList()) {
launch {
database.person
.selectById(PersonQuery.SelectById.Params(id = personId))
.asFlow()
.cancellable()
.collect { personResult ->
person = personResult.firstOrNull()
}
}
launch {
database.comment
.selectByPersonId(CommentQuery.SelectByPersonId.Params(personId = personId))
.asFlow()
.cancellable()
.collect { commentList ->
comments = commentList
}
}
}
}
Performance tips
- Use
LaunchedEffectin Compose to automatically cancel collectors when the composable leaves composition. - Database work is already executed on the database’s single-thread dispatcher, so there’s no need to add
flowOn(Dispatchers.IO). - Debounce high-frequency updates with
.debounce(100)if needed. - Scope queries as tightly as possible instead of broad
SELECT *operations.
Cascade notifications
SQLite foreign keys can cascade deletes or updates into other tables. SQLiteNow surfaces those
changes through affectedTables so that reactive flows re-run automatically. To describe the
relationships, add a cascadeNotify block to your table definition; the generator reads that
metadata and follows the transitive chain.
-- person.sql
-- @@{
-- cascadeNotify = {
-- delete = ["person_address", "person_phone"],
-- update = ["person_phone"]
-- }
-- }
CREATE TABLE person (
id INTEGER PRIMARY KEY NOT NULL,
first_name TEXT NOT NULL
);
-- person_address.sql
-- @@{
-- cascadeNotify = {
-- delete = ["person_address_note"],
-- update = ["person_address_note"]
-- }
-- }
CREATE TABLE person_address (
id INTEGER PRIMARY KEY NOT NULL,
person_id INTEGER NOT NULL,
street TEXT NOT NULL,
FOREIGN KEY (person_id) REFERENCES person(id)
ON DELETE CASCADE
ON UPDATE CASCADE
);
-- person_address_note.sql
CREATE TABLE person_address_note (
id INTEGER PRIMARY KEY NOT NULL,
address_id INTEGER NOT NULL,
note TEXT NOT NULL,
FOREIGN KEY (address_id) REFERENCES person_address(id)
ON DELETE CASCADE
);
In this setup:
- Deleting a person notifies flows watching
person,person_address, andperson_address_note. - Updating a person notifies
personandperson_address(and, through the second block,person_address_note). - The generator computes the transitive closure, so you only need to list the first hop for each action.
Tip: When using SQLiteNow’s sync features, flows refresh during sync operations automatically. See Reactive Sync Updates for sync-specific patterns.