Using transactions to group changes

Use writeTransaction to group statements that can write to the database.

database.writeTransaction {
    database.execute(
        sql = "DELETE FROM list WHERE id = ?",
        parameters = listOf(listId)
    )
    database.execute(
        sql = "DELETE FROM todos WHERE list_id = ?",
        parameters = listOf(listId)
    )
}

Subscribe to changes in data

Use the watch method to watch for changes to the dependent tables of any SQL query.

// You can watch any SQL query
fun watchCustomers(): Flow<List<User>> {
    // TODO: implement your UI based on the result set
    return database.watch("SELECT * FROM customers", mapper = { cursor ->
        User(
            id = cursor.getString(0)!!,
            name = cursor.getString(1)!!,
            email = cursor.getString(2)!!
        )
    })
}

Insert, update, and delete data in the local database

Use execute to run INSERT, UPDATE or DELETE queries.

suspend fun updateCustomer(id: String, name: String, email: String) {
    database.execute(
        "UPDATE customers SET name = ? WHERE email = ?",
        listOf(name, email)
    )
}

Send changes in local data to your backend service

Override uploadData to send local updates to your backend service. If you are using Supabase, see SupabaseConnector.kt for a complete implementation.

/**
 * This function is called whenever there is data to upload, whether the device is online or offline.
 * If this call throws an error, it is retried periodically.
 */
override suspend fun uploadData(database: PowerSyncDatabase) {

    val transaction = database.getNextCrudTransaction() ?: return;

    var lastEntry: CrudEntry? = null;
    try {

        for (entry in transaction.crud) {
            lastEntry = entry;

            val table = supabaseClient.from(entry.table)
            when (entry.op) {
                UpdateType.PUT -> {
                    val data = entry.opData?.toMutableMap() ?: mutableMapOf()
                    data["id"] = entry.id
                    table.upsert(data)
                }

                UpdateType.PATCH -> {
                    table.update(entry.opData!!) {
                        filter {
                            eq("id", entry.id)
                        }
                    }
                }

                UpdateType.DELETE -> {
                    table.delete {
                        filter {
                            eq("id", entry.id)
                        }
                    }
                }
            }
        }

        transaction.complete(null);

    } catch (e: Exception) {
        println("Data upload error - retrying last entry: ${lastEntry!!}, $e")
        throw e
    }
}

Accessing PowerSync connection status information

// Intialize the DB
val db = remember { PowerSyncDatabase(factory, schema) }
// Get the status as a flow
val status = db.currentStatus.asFlow().collectAsState(initial = null)
// Use the emitted values from the flow e.g. to check if connected
val isConnected = status.value?.connected

Wait for the initial sync to complete

Use the hasSynced property and register a listener to indicate to the user whether the initial sync is in progress.

val db = remember { PowerSyncDatabase(factory, schema) }
val status = db.currentStatus.asFlow().collectAsState(initial = null)
val hasSynced by remember { derivedStateOf { status.value?.hasSynced } }

when {
    hasSynced == null || hasSynced == false -> {
        Box(
            modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
            contentAlignment = Alignment.Center
        ) {
                Text(
                    text = "Busy with initial sync...",
                    style = MaterialTheme.typography.h6
                )
            }
    }
    else -> {
    ... show rest of UI

For async use cases, use waitForFirstSync method which is a suspense function that resolves once the first full sync has completed.

Using logging to troubleshoot issues

You can include your own Logger that must conform to the Kermit Logger as shown here.

PowerSyncDatabase(
  ...
  logger: Logger? = YourLogger
)

If you don’t supply a Logger then a default Kermit Logger is created with settings to only show Warnings in release and Verbose in debug as follows:

val defaultLogger: Logger = Logger

// Severity is set to Verbose in Debug and Warn in Release
if(BuildConfig.isDebug) {
    Logger.setMinSeverity(Severity.Verbose)
} else {
    Logger.setMinSeverity(Severity.Warn)
}

return defaultLogger

You are able to use the Logger anywhere in your code as follows to debug:

import co.touchlab.kermit.Logger

Logger.i("Some information");
Logger.e("Some error");
...