sqldelight

SQLDelight Multiplatform Tutorial: How to Use Shared Databases in KMP Apps

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
sqldelight

What is SQLDelight?

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.

Why Use SQLDelight in Multiplatform Apps?

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.

Setting Up Your Kotlin Multiplatform Project

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.

Steps to Create a Project:

  1. Open Android Studio.
  2. Click New Project.
  3. Select Kotlin Multiplatform App.
  4. Name it something like MyMultiApp.
  5. Complete the setup.

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.

Adding SQLDelight to Your Project

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.

Creating Your Database Schema

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.

Writing Queries

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.

Setting Up the Database Instance

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:

  • Android (androidMain):
actual class DatabaseDriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver {
        return AndroidSqliteDriver(Database.Schema, context, "mydb.db")
    }
}
  • iOS (iosMain):
actual class DatabaseDriverFactory {
    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(Database.Schema, "mydb.db")
    }
}
  • Desktop (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

Using SQLDelight in Your App

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.

Building the Android Part

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")
                }
            }
        }
    }
}

Handling iOS Integration

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.

Adding More Features: Migrations

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.

Querying with Parameters

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.

Transactions

For complex operations, use transactions to ensure data consistency:

queries.transaction {
    // Multiple operations here
}

If an error occurs, it rolls back the changes.

Coroutines and Flow

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.

Performance Tips

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.

Common Mistakes for Beginners

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.

Advanced Topics: Custom Types

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.

Testing Your Database

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)

Real-World Example: Full Todo App

Combine everything for a complete app. Use the schema, repository, and UI code above. This demonstrates how SQLDelight multiplatform shares the data layer.

Syncing Data Across Devices

For cloud sync, add a service like Firebase. Upload changes and observe queries to sync diffs—though this is an advanced topic.

Comparing with Other Libraries

Compared to Room (Android-only) or Realm (iOS-friendly but not shared), SQLDelight fills the multiplatform gap effectively.

Community and Resources

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.

Wrapping Up

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!

FAQs

What is SQLDelight, and why should I use it for multiplatform apps?

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.

Do I need advanced database knowledge to use SQLDelight?

No, you don’t! Basic SQL skills are enough. SQLDelight handles the complex parts, and this guide provides step-by-step examples for beginners.

How do I set up a Kotlin Multiplatform project with SQLDelight?

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.

What platforms does SQLDelight support?

SQLDelight works with Android, iOS, desktop, and web platforms that support SQLite, making it versatile for Kotlin Multiplatform projects.

Can I use SQLDelight with Jetpack Compose?

Yes! The blog includes a Jetpack Compose example for the Android UI, and SQLDelight integrates well with Compose for reactive updates using Flows.

How do I handle database migrations with SQLDelight?

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.

What are some common mistakes to avoid with SQLDelight?

Common pitfalls include forgetting to rebuild after changing .sq files and mismatching SQL and Kotlin types. Always check the generated code for errors.

How can I test my SQLDelight database?

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.

Is it possible to sync data across devices with SQLDelight?

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.

Where can I find more help with SQLDelight multiplatform?

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.