Android SQLite

Use SQLite on Android

An Android application uses KPojo models with an app-owned KronosDataSourceWrapper for SQLiteDatabase.

Add Kronos

Configure the standard google(), mavenCentral(), and gradlePluginPortal() repositories, then add the Kronos plugin and core library to the Android project.

plugins {
    id("com.android.application") version "8.13.2" apply false
    id("org.jetbrains.kotlin.android") version "2.4.21" apply false
    id("com.kotlinorm.kronos-gradle-plugin") version "0.4.0" apply false
}

These snippets use Android Gradle Plugin 8.13.2 and Kotlin 2.4.21. The reference application uses minSdk 26 and JDK 17.

Apply the plugin to every Android application or library module that declares a KPojo.

plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("com.kotlinorm.kronos-gradle-plugin")
}

kotlin {
    compilerOptions {
        freeCompilerArgs.addAll("-Xcollection-literals", "-Xcontext-sensitive-resolution")
    }
}

dependencies {
    implementation("com.kotlinorm:kronos-core:0.4.0")
}

Define a model

Declare Android tables with the same model annotations used by other Kotlin applications.

import com.kotlinorm.annotations.PrimaryKey
import com.kotlinorm.annotations.Table
import com.kotlinorm.interfaces.KPojo

@Table("notes")
data class Note(
    @PrimaryKey(identity = true)
    var id: Long? = null,
    var title: String? = null,
    var content: String? = null,
    var favorite: Boolean? = false,
) : KPojo

Connect SQLiteDatabase

Add a KronosDataSourceWrapper implementation to the application. The Android reference wrapper is an application source file that you can copy into the project and adapt for its database name and version.

With that source available as AndroidSQLiteDataSourceWrapper, create it once in the application's onCreate and make it the Kronos data source.

import android.app.Application
import com.kotlinorm.Kronos
import com.kotlinorm.orm.ddl.table
import java.util.concurrent.Executors
import java.util.concurrent.Future

class NotesApplication : Application() {
    lateinit var database: AndroidSQLiteDataSourceWrapper
        private set
    private val schemaExecutor = Executors.newSingleThreadExecutor()
    private lateinit var schemaReady: Future<*>

    override fun onCreate() {
        super.onCreate()
        database = AndroidSQLiteDataSourceWrapper(this)
        Kronos.dataSource = { database }
        schemaReady = schemaExecutor.submit {
            database.table.syncTable<Note>()
        }
    }

    fun awaitSchemaReady() {
        schemaReady.get()
    }
}

Register the application class in AndroidManifest.xml.

<application
    android:name=".NotesApplication"
    android:label="@string/app_name"
    android:theme="@style/Theme.App">
    ...
</application>

Applications that already have an Application class can put the data-source setup there.

Log SQL to Logcat

Add kronos-logging when the application should send Kronos SQL logs to Logcat.

dependencies {
    implementation("com.kotlinorm:kronos-logging:0.4.0")
}

Call the setup function from NotesApplication.onCreate.

import com.kotlinorm.KronosLoggerApp

KronosLoggerApp.detectLoggerImplementation()

Use CRUD after the schema is ready

NotesApplication starts schema preparation once. Run database work on an executor and call awaitSchemaReady() before each operation. The example posts results and errors to the main thread.

import android.app.Activity
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.kotlinorm.Kronos
import com.kotlinorm.orm.ddl.table
import com.kotlinorm.orm.insert.insert
import com.kotlinorm.orm.select.select
import java.util.concurrent.Executors

class NotesActivity : Activity() {
    private val databaseExecutor = Executors.newSingleThreadExecutor()
    private val mainHandler = Handler(Looper.getMainLooper())

    private fun <T> runDatabase(
        action: () -> T,
        onSuccess: (T) -> Unit,
        onError: (Throwable) -> Unit,
    ) {
        databaseExecutor.execute {
            runCatching {
                (application as NotesApplication).awaitSchemaReady()
                action()
            }.onSuccess { result ->
                mainHandler.post { onSuccess(result) }
            }.onFailure { error ->
                mainHandler.post { onError(error) }
            }
        }
    }

    fun createNote() {
        runDatabase(
            action = {
                val id = Note(title = "First note", content = "Hello Android")
                    .insert()
                    .withId()
                    .execute()
                    .lastInsertId
                    ?: error("SQLite did not return the generated note id")

                Note()
                    .select()
                    .where { it.id == id }
                    .first()
            },
            onSuccess = { note ->
                // Render note in the UI.
            },
            onError = ::reportDatabaseError,
        )
    }

    private fun reportDatabaseError(error: Throwable) {
        Log.e("Notes", "Database operation failed", error)
    }

    override fun onDestroy() {
        databaseExecutor.shutdownNow()
        super.onDestroy()
    }
}

Use Kronos.transact for related writes. Android SQLite uses the default transaction settings.

In the same activity, call the runDatabase helper from the previous example.

import com.kotlinorm.Kronos
import com.kotlinorm.orm.insert.insert
import com.kotlinorm.orm.update.update

fun createFavoriteNote() {
    runDatabase(
        action = {
            Kronos.transact {
                val id = Note(title = "Plan", content = "Review Android support")
                    .insert()
                    .withId()
                    .execute()
                    .lastInsertId
                    ?: error("SQLite did not return the generated note id")

                Note(id = id)
                    .update()
                    .set { it.favorite = true }
                    .by { it.id }
                    .execute()
            }
        },
        onSuccess = {},
        onError = ::reportDatabaseError,
    )
}

Reference application

kronos-example-android contains a complete Markdown notebook with the wrapper source, model, repository, UI, and instrumentation tests.