Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

Hello everyone! If you’re new to building apps that work across different platforms like Android and iOS, you might have come across SQLDelight for multiplatform development. This guide is designed for beginners who want to learn step by step. We’ll explore what SQLDelight is, why it’s a great fit for multiplatform projects, and how to build a simple project with clear code examples. By the end, you’ll have a solid understanding of how to add a database to your app that works seamlessly across platforms.
When I first dipped my toes into multiplatform development, managing data storage felt overwhelming, especially with different codebases for each platform. SQLDelight changed that experience for me. It allows you to write SQL code once and use it everywhere, making your project cleaner and easier to maintain. If you’re searching for a beginner-friendly introduction to SQLDelight multiplatform, this article is for you.

SQLDelight is a tool created by Square that helps you work with databases using Kotlin. Its standout feature is that it generates Kotlin code from your SQL files, giving you type-safe queries without the hassle of extra code.
In simple terms, you define your database structure and queries in SQL files. SQLDelight then transforms these into Kotlin classes and functions that you can call in your app. This process handles the database interactions for you.
This is especially useful for multiplatform development with Kotlin Multiplatform, which lets you share code between Android, iOS, desktop, and even web. SQLDelight supports all these targets by leveraging SQLite, a lightweight database engine built into most systems, so no extra setup is required.
Many apps need to store data locally, like a note-taking app or a to-do list, where data should sync across devices but also work offline. Traditionally, you might use Room for Android and Core Data for iOS, which means writing similar logic twice. With SQLDelight multiplatform, you can share your database code across platforms, saving time and reducing the chance of errors.
It’s also fast because it uses SQLite directly, and since the code is generated, you catch mistakes during compilation rather than at runtime. For beginners, the best part is how straightforward it is to get started—you don’t need advanced database skills, just basic SQL knowledge.
Before adding SQLDelight, let’s set up a basic Kotlin Multiplatform project. We’ll use Kotlin version 1.9 or later, which offers excellent multiplatform support.
First, install IntelliJ IDEA or Android Studio, as these tools simplify multiplatform development.
MyMultiApp.This creates a project with a shared common module for code that works across platforms, plus platform-specific modules for Android and iOS.
In the build.gradle.kts file for the shared module, you’ll see targets like android, iosX64, and others. Run the project to test it—on Android, it builds an APK, while for iOS, you’ll need Xcode on a Mac. If you’re on Windows, you can focus on Android and desktop for now.
To integrate SQLDelight, update your build files.
In the root build.gradle.kts, add the SQLDelight plugin:
plugins {
id("com.squareup.sqldelight") version "2.0.2" // Use the latest version as of 2025
}
Then, in the shared module’s build.gradle.kts, add the dependency:
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.squareup.sqldelight:runtime:2.0.2")
}
}
val androidMain by getting {
dependencies {
implementation("com.squareup.sqldelight:android-driver:2.0.2")
}
}
val iosMain by getting {
dependencies {
implementation("com.squareup.sqldelight:native-driver:2.0.2")
}
}
val jvmMain by getting {
dependencies {
implementation("com.squareup.sqldelight:sqlite-driver:2.0.2")
}
}
}
Sync your project after these changes. Now SQLDelight is ready to use.
SQLDelight uses .sq files to define your database schema. Create a folder in the shared module: src/commonMain/sqldelight/com/example/mymultiapp/db. Inside, create a file named Database.sq.
For a simple to-do app, add this schema:
CREATE TABLE Todo (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0
);
This creates a table with id, title, and completed fields. SQLDelight will generate a Todo data class from this schema.
Below the schema in the Database.sq file, add your queries:
-- Insert a new todo
insertTodo:
INSERT INTO Todo (title, completed)
VALUES (?, ?);
-- Fetch all todos
selectAllTodos:
SELECT * FROM Todo;
-- Fetch a todo by ID
selectTodoById:
SELECT * FROM Todo WHERE id = ?;
-- Update a todo's completion status
updateTodo:
UPDATE Todo SET completed = ? WHERE id = ?;
-- Delete a todo by ID
deleteTodo:
DELETE FROM Todo WHERE id = ?;
These are named queries, and SQLDelight uses the names to create corresponding functions. Build the project to generate the code in the build folder, where you’ll find Database.kt with your queries.
To use the database, create a driver for each platform. In the shared commonMain, create a file called DatabaseHelper.kt:
expect class DatabaseDriverFactory {
fun createDriver(): SqlDriver
}
Then, implement it for each platform:
androidMain):actual class DatabaseDriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(Database.Schema, context, "mydb.db")
}
}
iosMain):actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(Database.Schema, "mydb.db")
}
}
jvmMain, optional):actual class DatabaseDriverFactory {
actual fun createDriver(): SqlDriver {
return JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
}
}
In your shared code, initialize the database:
val driver = DatabaseDriverFactory().createDriver()
val database = Database(driver)
val queries = database.todoQueries
Let’s add code to insert and read todos. In the commonMain, create a repository file called TodoRepository.kt:
class TodoRepository(private val queries: TodoQueries) {
fun addTodo(title: String) {
queries.insertTodo(title, 0)
}
fun getAllTodos(): List<Todo> {
return queries.selectAllTodos().executeAsList()
}
fun toggleCompleted(id: Long, completed: Boolean) {
queries.updateTodo(if (completed) 1 else 0, id)
}
fun deleteTodo(id: Long) {
queries.deleteTodo(id)
}
}
This repository is shared across all platforms.
For Android, create a ViewModel in androidMain:
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collectAsState
class TodoViewModel(private val repo: TodoRepository) : ViewModel() {
val todos = MutableStateFlow(repo.getAllTodos())
fun addTodo(title: String) {
repo.addTodo(title)
todos.value = repo.getAllTodos()
}
fun toggleCompleted(id: Long, completed: Boolean) {
repo.toggleCompleted(id, completed)
todos.value = repo.getAllTodos()
}
fun deleteTodo(id: Long) {
repo.deleteTodo(id)
todos.value = repo.getAllTodos()
}
}
For the UI, use Jetpack Compose, which works well with multiplatform projects. Create a TodoScreen.kt file:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material.Button
import androidx.compose.material.Checkbox
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@Composable
fun TodoScreen(viewModel: TodoViewModel) {
val todos by viewModel.todos.collectAsState()
var title by remember { mutableStateOf("") }
Column {
TextField(
value = title,
onValueChange = { title = it },
label = { Text("New Todo") }
)
Button(onClick = {
if (title.isNotEmpty()) {
viewModel.addTodo(title)
title = ""
}
}) {
Text("Add")
}
todos.forEach { todo ->
Row {
Checkbox(
checked = todo.completed == 1,
onCheckedChange = { viewModel.toggleCompleted(todo.id, it) }
)
Text(todo.title)
Button(onClick = { viewModel.deleteTodo(todo.id) }) {
Text("Delete")
}
}
}
}
}
For iOS, you’ll need Swift or Objective-C to call the Kotlin code. Kotlin Multiplatform exports the shared module as a framework. In Xcode, add this framework.
In Swift, set up the database:
let driverFactory = DatabaseDriverFactory()
let db = Database(driver: driverFactory.createDriver())
let queries = db.todoQueries
For a simple SwiftUI view:
import SwiftUI
struct TodoView: View {
@State private var todos: [Todo] = []
@State private var title: String = ""
var body: some View {
VStack {
TextField("New Todo", text: $title)
Button(action: {
if !title.isEmpty {
queries.insertTodo(title: title, completed: 0)
title = ""
todos = queries.selectAllTodos().executeAsList()
}
}) {
Text("Add")
}
List(todos, id: \.id) { todo in
HStack {
Toggle(isOn: Binding(
get: { todo.completed == 1 },
set: { queries.updateTodo(completed: $0 ? 1 : 0, id: todo.id) }
)) {}
Text(todo.title)
Button("Delete") {
queries.deleteTodo(id: todo.id)
todos = queries.selectAllTodos().executeAsList()
}
}
}
}
}
}
You may need to add a refresh mechanism, like a timer, to update the list.
As your app grows, you might need to update the schema. SQLDelight supports migrations. In Database.sq, add:
CREATE TABLE V2 AS
migration_1_2:
ALTER TABLE Todo ADD COLUMN description TEXT;
Update the schema version in Database.Schema to 2. When creating the driver, SQLDelight handles the migration automatically, preserving your data.
SQLDelight simplifies parameterized queries. For selectTodoById, it generates:
queries.selectTodoById(id: 1).executeAsOneOrNull()
This returns a Todo object or null if not found. The Todo class includes id, title, and completed.
For complex operations, use transactions to ensure data consistency:
queries.transaction {
// Multiple operations here
}
If an error occurs, it rolls back the changes.
SQLDelight integrates with Kotlin Flows for reactive updates:
fun getAllTodosFlow(): Flow<List<Todo>> {
return queries.selectAllTodos().asFlow().mapToList(Dispatchers.Default)
}
In Android, use collectAsState() to update the UI. For iOS, you can bridge this with Combine, though it requires additional setup.
For large datasets, index your tables:
CREATE INDEX idx_title ON Todo(title);
Use LIMIT for paging:
selectPaginated:
SELECT * FROM Todo LIMIT ? OFFSET ?;
Call it with page size and offset values.
A common mistake is forgetting to rebuild after changing .sq files—always rebuild to generate new code. Another is mismatched types: SQL INTEGER maps to Kotlin Long, TEXT to String, and REAL to Double. Check the generated code if you encounter errors.
You can map custom types, like a Date column, using adapters:
class DateAdapter : ColumnAdapter<Date, Long> {
override fun decode(databaseValue: Long) = Date(databaseValue)
override fun encode(value: Date) = value.time
}
In the schema:
date: INTEGER AS Date NOT NULL
Add the adapter to Database.Schema.
Test in commonTest with an in-memory driver:
val driver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
Database.Schema.create(driver)
val db = Database(driver)
assertEquals(0, db.todoQueries.selectAllTodos().executeAsList().size)
Combine everything for a complete app. Use the schema, repository, and UI code above. This demonstrates how SQLDelight multiplatform shares the data layer.
For cloud sync, add a service like Firebase. Upload changes and observe queries to sync diffs—though this is an advanced topic.
Compared to Room (Android-only) or Realm (iOS-friendly but not shared), SQLDelight fills the multiplatform gap effectively.
Check the official docs on GitHub (square/sqldelight) for samples. Join Kotlin Slack for support. As of September 2025, version 2 offers improved Compose integration.
This guide covered SQLDelight multiplatform from setup to advanced use. Start with a small project and build from there. If you have questions, leave a comment below!
SQLDelight is a tool by Square that generates Kotlin code from SQL files, offering type-safe queries. It’s great for multiplatform apps because it lets you share database code across Android, iOS, and other platforms, saving time and reducing errors.
No, you don’t! Basic SQL skills are enough. SQLDelight handles the complex parts, and this guide provides step-by-step examples for beginners.
Start by creating a new Kotlin Multiplatform project in Android Studio. Then, add the SQLDelight plugin and dependencies in your build.gradle.kts files, as shown in the setup section of the blog.
SQLDelight works with Android, iOS, desktop, and web platforms that support SQLite, making it versatile for Kotlin Multiplatform projects.
Yes! The blog includes a Jetpack Compose example for the Android UI, and SQLDelight integrates well with Compose for reactive updates using Flows.
Add migration statements in your .sq file, like ALTER TABLE, and update the schema version. SQLDelight manages the migration process automatically when you recreate the driver.
Common pitfalls include forgetting to rebuild after changing .sq files and mismatching SQL and Kotlin types. Always check the generated code for errors.
Use the in-memory driver in your test code, as shown in the testing section. This lets you run tests on the JVM and verify your queries.
SQLDelight doesn’t handle sync itself, but you can add a service like Firebase. Upload changes and observe queries to sync data, as discussed in the advanced section.
Check the official GitHub docs (square/sqldelight) for samples, or join the Kotlin Slack community. The blog also highlights resources available as of September 2025.