
Enhances SQLDelight with a driver wrapping AndroidX SQLite libraries, simplifying database management via the AndroidxSqliteDriver factory, handling migrations seamlessly.
The SQLDelight AndroidX Driver provides a SQLDelight driver that wraps AndroidX Kotlin Multiplatform SQLite libraries. It works with any of the available implementations of AndroidX SQLite.
[!TIP] Interested in trying out web (
wasmJs/js) support? An alpha is in progress on thewebbranch — feedback welcome.
[!NOTE]
If you are migrating from version 0.0.17 or earlier, check out the migration guide
First setup SQLite dependencies following Set up SQLite for KMP.
Next add the dependency to your project.
repositories {
mavenCentral()
}
// Android/JVM
dependencies {
implementation("com.eygraber:sqldelight-androidx-driver:0.1.1")
}
// Multiplatform
commonMain.dependencies {
implementation("com.eygraber:sqldelight-androidx-driver:0.1.1")
}See Consuming Via Gradle for how to add a -SNAPSHOT release.
Next configure the database.
[!IMPORTANT]
generateAsyncmust equaltruesinceAndroidxSqliteDriveris a suspending driver.
sqldelight {
databases {
create("Database") {
generateAsync = true
}
}
}[!TIP] AndroidX team recommends
BundledSQLiteDriverbut you can use whichever one suits your needs. See SQLite Driver Implementations for more info.
Android or JVM - Pass a File:
val driver = AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.File(File("my.db")),
schema = Database.Schema,
)
val database = Database(driver)
// More database stuff...Android - Use Context to create the file in the app's database directory:
val driver = AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.FileProvider(context, "my.db"),
schema = Database.Schema,
)
val database = Database(driver)
// More database stuff...Multiplatform - Create a database type factory:
// src/commonMain/kotlin
expect class DatabaseTypeFactory {
fun createDatabaseType(): AndroidxSqliteDatabaseType
}
fun createDatabase(databaseTypeFactory: DatabaseTypeFactory): Database {
val driver = AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = databaseTypeFactory.createDatabaseType(),
schema = Database.Schema
)
val database = Database(driver)
// More database stuff...
}
// src/androidMain/kotlin
actual class DatabaseTypeFactory(private val context: Context) {
actual fun createDatabaseType(): AndroidxSqliteDatabaseType {
return AndroidxSqliteDatabaseType.FileProvider(context, "my.db")
}
}
// src/nativeMain/kotlin
actual class DatabaseTypeFactory {
actual fun createDatabaseType(): AndroidxSqliteDatabaseType {
return AndroidxSqliteDatabaseType.File("<absolute path to db file>")
}
}
// src/jvmMain/kotlin
actual class DatabaseTypeFactory {
actual fun createDatabaseType(): AndroidxSqliteDatabaseType {
AndroidxSqliteDatabaseType.File(File("my.db"))
}
}If you want to provide OpenFlags to the bundled or native driver, you can use:
Database(
AndroidxSqliteDriver(
connectionFactory = object : AndroidxSqliteConnectionFactory {
override val driver = BundledSQLiteDriver()
override fun createConnection(name: String) =
driver.open(name, SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE)
},
databaseType = AndroidxSqliteDatabaseType.File("<absolute path to db file>"),
schema = Database.Schema,
)
)It will handle calling the create and migrate functions on your schema for you, and keep track of the database's version.
AndroidxSqliteDriver is a suspending driver, so SQLDelight must be configured with
generateAsync = true:
sqldelight {
databases {
create("Database") {
generateAsync = true
}
}
}With generateAsync = true, generated queries return QueryResult.AsyncValue rather than
materialized values — you must call .await() (or the awaitAsOne/awaitAsList/etc. helpers
from app.cash.sqldelight:async-extensions) to get the result:
val user: User = database.userQueries.selectById(id).awaitAsOne()
val users: List<User> = database.userQueries.selectAll().awaitAsList()All database calls are suspending, and the driver runs them on its own coroutine dispatchers —
you don't need to wrap calls in withContext(Dispatchers.IO). Learn more below.
AndroidxSqliteDriver accepts four optional callbacks, all invoked at most once on the first
interaction with the database:
AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.File("my.db"),
schema = Database.Schema,
onConfigure = {
// Suspending. Runs before create/migrate. Use it to override pragmas that aren't
// covered by AndroidxSqliteConfiguration.
setForeignKeyConstraintsEnabled(true)
},
onCreate = { /* side effects after first create */ },
onUpdate = { old, new -> /* side effects after migration */ },
onOpen = { /* side effects after create or migrate */ },
)All four callbacks run at most once per driver instance, on the first database interaction,
guarded by an internal mutex — concurrent first-time callers all wait for them to complete before
any queries execute. If you close the driver and construct a new one against the same database,
onConfigure and onOpen run again for the new instance; onCreate and onUpdate only run if
the schema actually needs to be created or migrated.
onConfigure runs first, before any schema work. It's the only suspending callback and is the
right place to override pragmas that aren't covered by AndroidxSqliteConfiguration.onCreate runs only when the database is created for the first time, after SqlSchema.create
has committed.onUpdate runs only when the schema version has increased, after SqlSchema.migrate has
committed.onOpen runs on every first interaction, after any create/migrate work.To seed data or run additional SQL during create or migrate, put it in your
SqlSchema.create / SqlSchema.migrate — those run inside a driver-managed transaction.
onCreate, onUpdate, and onOpen are non-suspending and meant for things like logging or
updating in-memory state.
A companion artifact provides Flow extensions that mirror
app.cash.sqldelight:coroutines-extensions,
but default the CoroutineContext parameter to EmptyCoroutineContext — the driver already
dispatches each query onto its own connection pool, so wrapping every mapper in a second
withContext(Dispatchers.IO) is redundant.
dependencies {
implementation("com.eygraber:sqldelight-coroutines-extensions:0.1.1")
}import com.eygraber.sqldelight.androidx.driver.coroutines.asFlow
import com.eygraber.sqldelight.androidx.driver.coroutines.mapToList
import com.eygraber.sqldelight.androidx.driver.coroutines.mapToOne
import com.eygraber.sqldelight.androidx.driver.coroutines.mapToOneOrNull
val usersFlow: Flow<List<User>> = database.userQueries.selectAll().asFlow().mapToList()
val userFlow: Flow<User?> = database.userQueries.selectById(id).asFlow().mapToOneOrNull()You can still pass an explicit CoroutineContext if you want the mapper to run somewhere
specific (e.g. Dispatchers.Main for UI state). If you prefer the sqldelight extensions,
they still work — just import from app.cash.sqldelight.coroutines instead.
When using AndroidxSqliteDriver, the handling of foreign key constraints during database creation and migration is
managed to ensure data integrity.
If you have foreign key constraints enabled in your
AndroidxSqliteConfiguration (i.e. isForeignKeyConstraintsEnabled = true),
the driver will automatically disable them before executing the schema create or migrate operations.
This is done to prevent issues with table creation order and data manipulation during the migration process.
After the creation or migration is complete, foreign key constraints are re-enabled.
Furthermore, to verify the integrity of the foreign key relationships after these operations,
the driver performs an additional check. If isForeignKeyConstraintsCheckedAfterCreateOrUpdate
is true (which it is by default), a PRAGMA foreign_key_check is executed. If this check finds
any violations, an AndroidxSqliteDriver.ForeignKeyConstraintCheckException is thrown, detailing the
specific constraints that have been violated. This helps catch any inconsistencies in your data that might
have been introduced during the migration.
[!IMPORTANT]
By default, the first 100 violations will be parsed out of the result set ofPRAGMA foreign_key_checkand stored in theAndroidxSqliteDriver.ForeignKeyConstraintCheckException. If your use can result in a large number of violations you can adjust the max amount that will be processed viaAndroidxSqliteConfiguration.maxMigrationForeignKeyConstraintViolationsToReport.
SQLite supports several concurrency models that can significantly impact your application's performance. This driver
provides flexible connection pooling through the AndroidxSqliteConcurrencyModel interface.
The simplest model with one connection handling all operations:
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.SingleReaderWriter()
)Best for:
Dedicated reader connections for read-only access:
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.MultipleReaders(
readerCount = 3 // Number of concurrent reader connections
)
)Best for:
Important: This model is designed for read-only access. No write operations (INSERT, UPDATE, DELETE) should be
performed. If you need write capabilities, use MultipleReadersSingleWriter in WAL mode instead.
The most flexible model that adapts based on journal mode:
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true, // Enable WAL mode for true concurrency
walCount = 3, // Reader connections when WAL is enabled (default: 3)
nonWalCount = 0, // Reader connections when WAL is disabled (default: 0)
)
)Best for:
PRAGMA synchronous = FULL is used)The optimal number of reader connections depends on your use case:
// Conservative (default)
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 3,
nonWalCount = 0,
)
// High-concurrency applications
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 8
)
// Memory-conscious applications
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 2
)On Android, you can use system-determined connection pool sizes:
// Based on SQLiteGlobal.getWALConnectionPoolSize()
fun getWALConnectionPoolSize(): Int {
val resources = Resources.getSystem()
val resId = resources.getIdentifier("db_connection_pool_size", "integer", "android")
return if (resId != 0) {
resources.getInteger(resId)
} else {
2 // Fallback default
}
}
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = getWALConnectionPoolSize(),
nonWalCount = 0,
)
)| Model | Memory Usage | Read Concurrency | Write Capability | Best Use Case |
|---|---|---|---|---|
| SingleReaderWriter | Lowest | None | Full | Simple apps |
| MultipleReaders | Medium | Excellent | None (read-only) | Read-only apps |
| MultipleReadersSingleWriter (WAL) | Higher | Excellent | Full | Production |
| MultipleReadersSingleWriter (non-WAL) | Medium | Limited | Full | Legacy/constrained |
[!NOTE]
In-Memory and temporary databases automatically useSingleReaderWritermodel regardless of configuration, as connection pooling provides no benefit for these database types.
The driver runs SQLite work on its own CoroutineDispatcher, sized to match the concurrency model
so a writer and each reader get their own slot. You don't need to use withContext(Dispatchers.IO)
when calling the driver — queries and transactions will suspend onto the driver's dispatcher
automatically, and switch back to your calling context when they return. Inside a transaction,
every operation stays on the same slot for the lifetime of the transaction, so you don't have to
worry about switching connections mid-transaction.
You pick how those threads are sourced by passing a dispatcherProvider when constructing the
concurrency model. Two are bundled:
// Default. Shares threads with Dispatchers.IO via limitedParallelism.
// Lower memory — no dedicated threads are created for the driver.
AndroidxSqliteConcurrencyModel.memoryOptimizedProvider()
// Allocates a dedicated thread pool (via newFixedThreadPoolContext).
// Each connection tends to stay on the same thread, which helps CPU cache locality
// at the cost of extra OS threads.
AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvidermemoryOptimizedProvider() also accepts a base dispatcher if you'd rather derive parallelism from
somewhere other than Dispatchers.IO:
AndroidxSqliteConcurrencyModel.memoryOptimizedProvider(
dispatcher = myBackgroundDispatcher,
)The provider plugs into any concurrency model:
// Single connection, single dispatcher slot
AndroidxSqliteConcurrencyModel.SingleReaderWriter(
dispatcherProvider = AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvider,
)
// Multiple readers (read-only), one slot per reader
AndroidxSqliteConcurrencyModel.MultipleReaders(
readerCount = 3,
dispatcherProvider = AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvider,
)
// Multiple readers + single writer
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 3,
dispatcherProvider = AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvider,
)driver.close() disposes the dispatcher, so you generally don't need to manage its lifetime.
If PRAGMA journal_mode = ... is executed through the driver, the connection pool will:
PRAGMA statementMultipleReadersSingleWriter model is in use, flip its isWal flag based on the
journal mode the statement returnedThis ensures all connections use the same journal mode and prevents inconsistencies.
MultipleReadersSingleWriter in WAL modeFor additional background on WAL mode and dispatcher tuning, see WAL & Dispatchers.
The SQLDelight AndroidX Driver provides a SQLDelight driver that wraps AndroidX Kotlin Multiplatform SQLite libraries. It works with any of the available implementations of AndroidX SQLite.
[!TIP] Interested in trying out web (
wasmJs/js) support? An alpha is in progress on thewebbranch — feedback welcome.
[!NOTE]
If you are migrating from version 0.0.17 or earlier, check out the migration guide
First setup SQLite dependencies following Set up SQLite for KMP.
Next add the dependency to your project.
repositories {
mavenCentral()
}
// Android/JVM
dependencies {
implementation("com.eygraber:sqldelight-androidx-driver:0.1.1")
}
// Multiplatform
commonMain.dependencies {
implementation("com.eygraber:sqldelight-androidx-driver:0.1.1")
}See Consuming Via Gradle for how to add a -SNAPSHOT release.
Next configure the database.
[!IMPORTANT]
generateAsyncmust equaltruesinceAndroidxSqliteDriveris a suspending driver.
sqldelight {
databases {
create("Database") {
generateAsync = true
}
}
}[!TIP] AndroidX team recommends
BundledSQLiteDriverbut you can use whichever one suits your needs. See SQLite Driver Implementations for more info.
Android or JVM - Pass a File:
val driver = AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.File(File("my.db")),
schema = Database.Schema,
)
val database = Database(driver)
// More database stuff...Android - Use Context to create the file in the app's database directory:
val driver = AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.FileProvider(context, "my.db"),
schema = Database.Schema,
)
val database = Database(driver)
// More database stuff...Multiplatform - Create a database type factory:
// src/commonMain/kotlin
expect class DatabaseTypeFactory {
fun createDatabaseType(): AndroidxSqliteDatabaseType
}
fun createDatabase(databaseTypeFactory: DatabaseTypeFactory): Database {
val driver = AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = databaseTypeFactory.createDatabaseType(),
schema = Database.Schema
)
val database = Database(driver)
// More database stuff...
}
// src/androidMain/kotlin
actual class DatabaseTypeFactory(private val context: Context) {
actual fun createDatabaseType(): AndroidxSqliteDatabaseType {
return AndroidxSqliteDatabaseType.FileProvider(context, "my.db")
}
}
// src/nativeMain/kotlin
actual class DatabaseTypeFactory {
actual fun createDatabaseType(): AndroidxSqliteDatabaseType {
return AndroidxSqliteDatabaseType.File("<absolute path to db file>")
}
}
// src/jvmMain/kotlin
actual class DatabaseTypeFactory {
actual fun createDatabaseType(): AndroidxSqliteDatabaseType {
AndroidxSqliteDatabaseType.File(File("my.db"))
}
}If you want to provide OpenFlags to the bundled or native driver, you can use:
Database(
AndroidxSqliteDriver(
connectionFactory = object : AndroidxSqliteConnectionFactory {
override val driver = BundledSQLiteDriver()
override fun createConnection(name: String) =
driver.open(name, SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE)
},
databaseType = AndroidxSqliteDatabaseType.File("<absolute path to db file>"),
schema = Database.Schema,
)
)It will handle calling the create and migrate functions on your schema for you, and keep track of the database's version.
AndroidxSqliteDriver is a suspending driver, so SQLDelight must be configured with
generateAsync = true:
sqldelight {
databases {
create("Database") {
generateAsync = true
}
}
}With generateAsync = true, generated queries return QueryResult.AsyncValue rather than
materialized values — you must call .await() (or the awaitAsOne/awaitAsList/etc. helpers
from app.cash.sqldelight:async-extensions) to get the result:
val user: User = database.userQueries.selectById(id).awaitAsOne()
val users: List<User> = database.userQueries.selectAll().awaitAsList()All database calls are suspending, and the driver runs them on its own coroutine dispatchers —
you don't need to wrap calls in withContext(Dispatchers.IO). Learn more below.
AndroidxSqliteDriver accepts four optional callbacks, all invoked at most once on the first
interaction with the database:
AndroidxSqliteDriver(
driver = BundledSQLiteDriver(),
databaseType = AndroidxSqliteDatabaseType.File("my.db"),
schema = Database.Schema,
onConfigure = {
// Suspending. Runs before create/migrate. Use it to override pragmas that aren't
// covered by AndroidxSqliteConfiguration.
setForeignKeyConstraintsEnabled(true)
},
onCreate = { /* side effects after first create */ },
onUpdate = { old, new -> /* side effects after migration */ },
onOpen = { /* side effects after create or migrate */ },
)All four callbacks run at most once per driver instance, on the first database interaction,
guarded by an internal mutex — concurrent first-time callers all wait for them to complete before
any queries execute. If you close the driver and construct a new one against the same database,
onConfigure and onOpen run again for the new instance; onCreate and onUpdate only run if
the schema actually needs to be created or migrated.
onConfigure runs first, before any schema work. It's the only suspending callback and is the
right place to override pragmas that aren't covered by AndroidxSqliteConfiguration.onCreate runs only when the database is created for the first time, after SqlSchema.create
has committed.onUpdate runs only when the schema version has increased, after SqlSchema.migrate has
committed.onOpen runs on every first interaction, after any create/migrate work.To seed data or run additional SQL during create or migrate, put it in your
SqlSchema.create / SqlSchema.migrate — those run inside a driver-managed transaction.
onCreate, onUpdate, and onOpen are non-suspending and meant for things like logging or
updating in-memory state.
A companion artifact provides Flow extensions that mirror
app.cash.sqldelight:coroutines-extensions,
but default the CoroutineContext parameter to EmptyCoroutineContext — the driver already
dispatches each query onto its own connection pool, so wrapping every mapper in a second
withContext(Dispatchers.IO) is redundant.
dependencies {
implementation("com.eygraber:sqldelight-coroutines-extensions:0.1.1")
}import com.eygraber.sqldelight.androidx.driver.coroutines.asFlow
import com.eygraber.sqldelight.androidx.driver.coroutines.mapToList
import com.eygraber.sqldelight.androidx.driver.coroutines.mapToOne
import com.eygraber.sqldelight.androidx.driver.coroutines.mapToOneOrNull
val usersFlow: Flow<List<User>> = database.userQueries.selectAll().asFlow().mapToList()
val userFlow: Flow<User?> = database.userQueries.selectById(id).asFlow().mapToOneOrNull()You can still pass an explicit CoroutineContext if you want the mapper to run somewhere
specific (e.g. Dispatchers.Main for UI state). If you prefer the sqldelight extensions,
they still work — just import from app.cash.sqldelight.coroutines instead.
When using AndroidxSqliteDriver, the handling of foreign key constraints during database creation and migration is
managed to ensure data integrity.
If you have foreign key constraints enabled in your
AndroidxSqliteConfiguration (i.e. isForeignKeyConstraintsEnabled = true),
the driver will automatically disable them before executing the schema create or migrate operations.
This is done to prevent issues with table creation order and data manipulation during the migration process.
After the creation or migration is complete, foreign key constraints are re-enabled.
Furthermore, to verify the integrity of the foreign key relationships after these operations,
the driver performs an additional check. If isForeignKeyConstraintsCheckedAfterCreateOrUpdate
is true (which it is by default), a PRAGMA foreign_key_check is executed. If this check finds
any violations, an AndroidxSqliteDriver.ForeignKeyConstraintCheckException is thrown, detailing the
specific constraints that have been violated. This helps catch any inconsistencies in your data that might
have been introduced during the migration.
[!IMPORTANT]
By default, the first 100 violations will be parsed out of the result set ofPRAGMA foreign_key_checkand stored in theAndroidxSqliteDriver.ForeignKeyConstraintCheckException. If your use can result in a large number of violations you can adjust the max amount that will be processed viaAndroidxSqliteConfiguration.maxMigrationForeignKeyConstraintViolationsToReport.
SQLite supports several concurrency models that can significantly impact your application's performance. This driver
provides flexible connection pooling through the AndroidxSqliteConcurrencyModel interface.
The simplest model with one connection handling all operations:
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.SingleReaderWriter()
)Best for:
Dedicated reader connections for read-only access:
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.MultipleReaders(
readerCount = 3 // Number of concurrent reader connections
)
)Best for:
Important: This model is designed for read-only access. No write operations (INSERT, UPDATE, DELETE) should be
performed. If you need write capabilities, use MultipleReadersSingleWriter in WAL mode instead.
The most flexible model that adapts based on journal mode:
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true, // Enable WAL mode for true concurrency
walCount = 3, // Reader connections when WAL is enabled (default: 3)
nonWalCount = 0, // Reader connections when WAL is disabled (default: 0)
)
)Best for:
PRAGMA synchronous = FULL is used)The optimal number of reader connections depends on your use case:
// Conservative (default)
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 3,
nonWalCount = 0,
)
// High-concurrency applications
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 8
)
// Memory-conscious applications
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 2
)On Android, you can use system-determined connection pool sizes:
// Based on SQLiteGlobal.getWALConnectionPoolSize()
fun getWALConnectionPoolSize(): Int {
val resources = Resources.getSystem()
val resId = resources.getIdentifier("db_connection_pool_size", "integer", "android")
return if (resId != 0) {
resources.getInteger(resId)
} else {
2 // Fallback default
}
}
AndroidxSqliteConfiguration(
concurrencyModel = AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = getWALConnectionPoolSize(),
nonWalCount = 0,
)
)| Model | Memory Usage | Read Concurrency | Write Capability | Best Use Case |
|---|---|---|---|---|
| SingleReaderWriter | Lowest | None | Full | Simple apps |
| MultipleReaders | Medium | Excellent | None (read-only) | Read-only apps |
| MultipleReadersSingleWriter (WAL) | Higher | Excellent | Full | Production |
| MultipleReadersSingleWriter (non-WAL) | Medium | Limited | Full | Legacy/constrained |
[!NOTE]
In-Memory and temporary databases automatically useSingleReaderWritermodel regardless of configuration, as connection pooling provides no benefit for these database types.
The driver runs SQLite work on its own CoroutineDispatcher, sized to match the concurrency model
so a writer and each reader get their own slot. You don't need to use withContext(Dispatchers.IO)
when calling the driver — queries and transactions will suspend onto the driver's dispatcher
automatically, and switch back to your calling context when they return. Inside a transaction,
every operation stays on the same slot for the lifetime of the transaction, so you don't have to
worry about switching connections mid-transaction.
You pick how those threads are sourced by passing a dispatcherProvider when constructing the
concurrency model. Two are bundled:
// Default. Shares threads with Dispatchers.IO via limitedParallelism.
// Lower memory — no dedicated threads are created for the driver.
AndroidxSqliteConcurrencyModel.memoryOptimizedProvider()
// Allocates a dedicated thread pool (via newFixedThreadPoolContext).
// Each connection tends to stay on the same thread, which helps CPU cache locality
// at the cost of extra OS threads.
AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvidermemoryOptimizedProvider() also accepts a base dispatcher if you'd rather derive parallelism from
somewhere other than Dispatchers.IO:
AndroidxSqliteConcurrencyModel.memoryOptimizedProvider(
dispatcher = myBackgroundDispatcher,
)The provider plugs into any concurrency model:
// Single connection, single dispatcher slot
AndroidxSqliteConcurrencyModel.SingleReaderWriter(
dispatcherProvider = AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvider,
)
// Multiple readers (read-only), one slot per reader
AndroidxSqliteConcurrencyModel.MultipleReaders(
readerCount = 3,
dispatcherProvider = AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvider,
)
// Multiple readers + single writer
AndroidxSqliteConcurrencyModel.MultipleReadersSingleWriter(
isWal = true,
walCount = 3,
dispatcherProvider = AndroidxSqliteConcurrencyModel.CpuCacheHitOptimizedProvider,
)driver.close() disposes the dispatcher, so you generally don't need to manage its lifetime.
If PRAGMA journal_mode = ... is executed through the driver, the connection pool will:
PRAGMA statementMultipleReadersSingleWriter model is in use, flip its isWal flag based on the
journal mode the statement returnedThis ensures all connections use the same journal mode and prevents inconsistencies.
MultipleReadersSingleWriter in WAL modeFor additional background on WAL mode and dispatcher tuning, see WAL & Dispatchers.