Quickstart

Table of contents
  1. 1. Define your model
  2. 2. Create a Sqkon instance
    1. Android
    2. JVM
  3. 3. Get a typed store
  4. 4. Insert, update, and delete
  5. 5. Query
  6. 6. Observe changes
  7. 7. Cleanup
  8. Next steps

This page walks you through inserting, querying, and observing data with Sqkon end-to-end. By the end you’ll have a working Merchant store backed by SQLite with JSONB queries and reactive Flow observation.

If you haven’t added the dependency yet, see Installation.

1. Define your model

Sqkon stores values by serializing them with kotlinx.serialization. Annotate your data class with @Serializable:

import kotlinx.serialization.Serializable

@Serializable
data class Merchant(
    val id: String,
    val name: String,
    val category: String,
)

Sqkon queries against the JSON representation of your value, so any field you want to filter, sort, or paginate on must be a serialized property — not a getter or computed field. See Serialization for the gotchas with sealed classes and inheritance.

2. Create a Sqkon instance

You need exactly one Sqkon per database. Treat it like a connection pool: construct it once during app startup and share it.

Android

class MyApplication : Application() {
    private val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

    val sqkon: Sqkon by lazy {
        Sqkon(
            context = this,
            scope = appScope,
            // dbFileName = null   // pass null for in-memory (testing)
        )
    }
}

JVM

import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteDatabaseType

val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

val sqkon = Sqkon(
    scope = appScope,
    type = AndroidxSqliteDatabaseType.File("sqkon.db"),
)

For tests, drop the type argument — the JVM default is AndroidxSqliteDatabaseType.Memory.

3. Get a typed store

Every type you want to persist gets its own KeyValueStorage. The store name is your “table” — pick something stable.

val merchants: KeyValueStorage<Merchant> =
    sqkon.keyValueStorage<Merchant>("merchants")

4. Insert, update, and delete

Write methods are blocking transactions on the calling thread (not suspend), but they’re cheap — Sqkon dispatches the SQLite work onto its internal write dispatcher under the hood. You can call them from any context, but for UI threads on Android you’ll typically still wrap them in launch { ... } / withContext(...) to keep the calling site uniformly async with the rest of your code.

val chipotle = Merchant(id = "1", name = "Chipotle", category = "Food")
val patagonia = Merchant(id = "2", name = "Patagonia", category = "Apparel")

// Insert
merchants.insert(key = chipotle.id, value = chipotle)

// Bulk insert
merchants.insertAll(
    mapOf(
        chipotle.id to chipotle,
        patagonia.id to patagonia,
    )
)

// Update existing or insert if missing
merchants.upsert(key = chipotle.id, value = chipotle.copy(name = "Chipotle Mexican Grill"))

// Delete by key
merchants.deleteByKey("2")

// Delete with a predicate
merchants.delete(where = Merchant::category eq "Food")

// Wipe the entire store
merchants.deleteAll()

5. Query

select returns a Flow that emits the current results and re-emits whenever matching rows change.

import kotlinx.coroutines.flow.first

// One-shot read of a single key
val one: Merchant? = merchants.selectByKey("1").first()

// Filter + sort + limit
val foodMerchants: Flow<List<Merchant>> = merchants.select(
    where = Merchant::category eq "Food",
    orderBy = listOf(OrderBy(Merchant::name, OrderDirection.ASC)),
    limit = 50,
)

// Combine predicates
val cheapEats = merchants.select(
    where = (Merchant::category eq "Food").and(Merchant::name like "Chi%")
)

// Count without materializing rows
val foodCount: Flow<Int> = merchants.count(where = Merchant::category eq "Food")

Where DSL operators: eq, neq, inList, notInList, like, gt, lt, plus not(...), .and(...), and .or(...). See the querying guide for the full reference.

6. Observe changes

Because select returns Flow, you can collect it from a ViewModel, LifecycleScope, or any coroutine context.

appScope.launch {
    merchants.selectAll().collect { all ->
        println("Store has ${all.size} merchants")
    }
}

Inserts, updates, and deletes from anywhere in your app will trigger a new emission automatically — Sqkon wires SQLDelight’s reactive query plumbing through the JSONB layer.

7. Cleanup

In tests, cancel the scope you passed to Sqkon to release the SQLite connection pool:

@After fun tearDown() {
    appScope.cancel()
}

In production, the application-scoped Sqkon lives for the process lifetime — no cleanup is needed.

Next steps