Driver to support SQLDelight generated code on Android
npx @tessl/cli install tessl/maven-app-cash-sqldelight--android-driver@2.1.0SQLDelight Android Driver provides a SQLite database driver implementation specifically designed for Android applications using SQLDelight. It serves as a bridge between SQLDelight's generated typesafe Kotlin APIs and Android's SQLite database system through the AndroidX SQLite library, with features like statement caching, transaction support, and reactive query listeners.
build.gradle.kts:
dependencies {
implementation("app.cash.sqldelight:android-driver:2.1.0")
api("androidx.sqlite:sqlite:2.3.1")
}import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import app.cash.sqldelight.db.SqlSchema
import app.cash.sqldelight.db.QueryResult
import android.content.Contextimport app.cash.sqldelight.driver.android.AndroidSqliteDriver
import app.cash.sqldelight.db.SqlSchema
import android.content.Context
// Create driver with schema and context
val driver = AndroidSqliteDriver(
schema = MyDatabase.Schema,
context = applicationContext,
name = "my_database.db"
)
// Use with SQLDelight generated database
val database = MyDatabase(driver)
// Perform database operations
val users = database.userQueries.selectAll().executeAsList()
// Close when done
driver.close()The Android Driver is built around several key components:
Create AndroidSqliteDriver instances for different use cases.
/**
* Create driver with schema, context, and optional configuration
*/
class AndroidSqliteDriver(
schema: SqlSchema<QueryResult.Value<Unit>>,
context: Context,
name: String? = null,
factory: SupportSQLiteOpenHelper.Factory = FrameworkSQLiteOpenHelperFactory(),
callback: SupportSQLiteOpenHelper.Callback = AndroidSqliteDriver.Callback(schema),
cacheSize: Int = 20,
useNoBackupDirectory: Boolean = false,
windowSizeBytes: Long? = null
)
/**
* Create driver from existing SupportSQLiteOpenHelper
*/
constructor(openHelper: SupportSQLiteOpenHelper)
/**
* Create driver from existing SupportSQLiteDatabase
*/
constructor(
database: SupportSQLiteDatabase,
cacheSize: Int = 20,
windowSizeBytes: Long? = null
)Parameters:
schema: SQLDelight schema containing DDL and migrationscontext: Android Context for database file accessname: Database file name (null for in-memory database)factory: Factory for creating SupportSQLiteOpenHelper (defaults to FrameworkSQLiteOpenHelperFactory)callback: Database lifecycle callback handler (defaults to AndroidSqliteDriver.Callback)cacheSize: Number of prepared statements to cache (defaults to 20)useNoBackupDirectory: Whether to prevent database backup (defaults to false)windowSizeBytes: Cursor window size in bytes for Android 28+ (defaults to null)Execute SQL statements and queries with parameter binding.
/**
* Execute SQL statement (INSERT, UPDATE, DELETE)
* @param identifier Optional cache key for statement caching
* @param sql SQL statement string
* @param parameters Number of bindable parameters
* @param binders Function to bind parameters to statement
* @return Number of affected rows
*/
fun execute(
identifier: Int?,
sql: String,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)? = null
): QueryResult<Long>
/**
* Execute SQL query (SELECT) and map results
* @param identifier Optional cache key for statement caching
* @param sql SQL query string
* @param mapper Function to map cursor results to return type
* @param parameters Number of bindable parameters
* @param binders Function to bind parameters to statement
* @return Mapped query results
*/
fun <R> executeQuery(
identifier: Int?,
sql: String,
mapper: (SqlCursor) -> QueryResult<R>,
parameters: Int,
binders: (SqlPreparedStatement.() -> Unit)? = null
): QueryResult<R>Manage database transactions with proper nesting support.
/**
* Start a new database transaction
* @return Transaction instance for controlling transaction lifecycle
*/
fun newTransaction(): QueryResult<Transacter.Transaction>
/**
* Get the currently active transaction
* @return Current transaction or null if none active
*/
fun currentTransaction(): Transacter.Transaction?Transaction Class:
inner class Transaction(
override val enclosingTransaction: Transacter.Transaction?
) : Transacter.Transaction() {
/**
* End the transaction
* @param successful Whether to commit (true) or rollback (false)
* @return QueryResult<Unit> indicating completion
*/
override fun endTransaction(successful: Boolean): QueryResult<Unit>
}Register listeners for reactive query result change notifications.
/**
* Add listener for query result changes
* @param queryKeys Variable number of query keys to listen for
* @param listener Listener to be notified of changes
*/
fun addListener(vararg queryKeys: String, listener: Query.Listener)
/**
* Remove previously registered listener
* @param queryKeys Variable number of query keys to stop listening for
* @param listener Listener to remove
*/
fun removeListener(vararg queryKeys: String, listener: Query.Listener)
/**
* Notify all registered listeners of query changes
* @param queryKeys Variable number of query keys that changed
*/
fun notifyListeners(vararg queryKeys: String)Properly close and cleanup database resources.
/**
* Close database connection and cleanup resources
* Evicts all cached statements and closes underlying database
*/
fun close()Handle database lifecycle events for schema creation and migration.
/**
* Database callback handler for schema creation and migration
*/
open class Callback(
private val schema: SqlSchema<QueryResult.Value<Unit>>,
private vararg val callbacks: AfterVersion
) : SupportSQLiteOpenHelper.Callback(schema.version.toInt()) {
/**
* Called when database is created for the first time
* @param db Database instance being created
*/
override fun onCreate(db: SupportSQLiteDatabase)
/**
* Called when database needs to be upgraded
* @param db Database instance being upgraded
* @param oldVersion Previous database version
* @param newVersion Target database version
*/
override fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int
)
}/**
* Interface for SQL cursor operations
*/
interface SqlCursor {
fun next(): QueryResult<Boolean>
fun getString(index: Int): String?
fun getLong(index: Int): Long?
fun getBytes(index: Int): ByteArray?
fun getDouble(index: Int): Double?
fun getBoolean(index: Int): Boolean?
}
/**
* Interface for prepared statement parameter binding
*/
interface SqlPreparedStatement {
fun bindBytes(index: Int, bytes: ByteArray?)
fun bindLong(index: Int, long: Long?)
fun bindDouble(index: Int, double: Double?)
fun bindString(index: Int, string: String?)
fun bindBoolean(index: Int, boolean: Boolean?)
}
/**
* Schema definition interface containing DDL and migrations
*/
interface SqlSchema<T> {
val version: Long
fun create(driver: SqlDriver): T
fun migrate(
driver: SqlDriver,
oldVersion: Long,
newVersion: Long,
vararg callbacks: AfterVersion
): T
}
/**
* Result wrapper type for database operations
*/
sealed interface QueryResult<out T> {
val value: T
object Unit : QueryResult<kotlin.Unit> {
override val value: kotlin.Unit = kotlin.Unit
}
data class Value<T>(override val value: T) : QueryResult<T>
}
/**
* Migration callback executed after version upgrade
*/
class AfterVersion(
val afterVersion: Long,
val block: (SqlDriver) -> kotlin.Unit
)
/**
* Query change listener interface
*/
interface Query.Listener {
fun queryResultsChanged()
}
/**
* Transaction interface for database transaction control
*/
abstract class Transacter.Transaction {
abstract val enclosingTransaction: Transacter.Transaction?
abstract fun endTransaction(successful: Boolean): QueryResult<kotlin.Unit>
}The Android Driver handles common database errors through the QueryResult type system and standard SQLite exceptions:
Common error scenarios:
The AndroidSqliteDriver is designed for multi-threaded access with the following guarantees:
Best practices: