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
        }
}

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 LaunchedEffect in 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, and person_address_note.
  • Updating a person notifies person and person_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.