Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.ditto.live/llms.txt

Use this file to discover all available pages before exploring further.

Overview

This guide covers the essential changes needed to migrate your Ditto Kotlin Android app from v4 to v5. The main architectural shift is moving from identity-based initialization to a three-phase configuration model with Flow-based APIs. Also required: v5 uses DQL (Ditto Query Language) for all data operations. See the DQL Migration Guide for query migration steps.

AI Agent Prompt

Use this prompt when working with an AI coding assistant to migrate your Ditto Kotlin Android app from v4 to v5.
I need help migrating a Ditto Kotlin/Android application from v4 to v5. Focus on these critical changes:

PACKAGE RENAME:
Replace all `live.ditto.*` imports with `com.ditto.kotlin.*`.

You must replace your definition in your gradle file(s):

```kotlin
//v5
ditto-kotlin = { module = "com.ditto:ditto-kotlin-android", version.ref = "ditto" }
```

```kotlin
//v4
live-ditto = { group = "live.ditto", name = "ditto", version.ref = "ditto" }
```

Also update the `implementation(libs.live.ditto)` dependency reference in your app's `build.gradle.kts`:
```kotlin
// v5
implementation(libs.ditto.kotlin)
// v4
implementation(libs.live.ditto)
```

Full import mapping:
- `live.ditto.*` → `com.ditto.kotlin.*`
- `live.ditto.transports.*` → `com.ditto.kotlin.transports.*`  (Note: `DittoSyncPermissions` is now in `com.ditto.kotlin.transports`)
- `live.ditto.android.*` → `com.ditto.kotlin.*` (Android-specific classes merged into main package)
- NEW: `com.ditto.kotlin.serialization.*` for `DittoCborSerializable`, `DittoJsonSerializable`, and `toDittoCbor()` extensions
- Use IDE Find & Replace: `live.ditto` → `com.ditto.kotlin` project-wide

Minimum Supported Android Version:
- Ditto SDK v5 requires `minSdk = 24` (Android 7.0 Nougat). If your project targets `minSdk = 23`, you must raise it in `build.gradle.kts`.

DittoInitializer (Android context setup):
- v5 automatically handles Android context via AndroidX App Startup through the SDK's own manifest merger. You do NOT need to manually add DittoInitializer to your AndroidManifest.xml. Remove any manual DittoInitializer manifest entries if present.

INITIALIZATION:
Replace `Ditto(dependencies, identity)` constructor and `Ditto.open(DittoAndroidConfig)` with `DittoFactory.create(DittoConfig(...))`
- `DittoFactory.create()` is NOT suspend — no coroutine needed
- `DittoConfig(databaseId, connect, ...)` replaces `DittoAndroidConfig(context, databaseId, connect, ...)`  — no `Context` parameter; Android context handled internally via `DittoInitializer` (AndroidX Startup)
- `DittoConfig.Connect.Server(url: String)` replaces `DittoConfigConnect.Server(url: URI)` — URL is now a `String`, not `java.net.URI`; class is nested inside `DittoConfig`
- `DittoConfig.Connect.SmallPeersOnly(privateKey?)` replaces `DittoConfigConnect.SmallPeersOnly(privateKey?)`
- `DittoConfig` properties are now `val` (immutable); `persistenceDirectory` is `String?` instead of `File?`
- Remove `DefaultAndroidDittoDependencies`, `AndroidDittoDependencies`, `DittoDependencies`, `DittoBase` — all removed
- Remove `DittoIdentity` sealed class and all subclasses (`OnlinePlayground`, `OnlineWithAuthentication`, `OfflinePlayground`, `SharedKey`, `Manual`) — all removed
- Remove `UpdateTransportConfig` websocket URL additions (cloud URL inferred from `DittoConfig.Connect.Server.url`)
- Remove `DisableSyncWithV3()`, `RunGarbageCollection()`, `GetTransportDiagnostics()` — all removed
- Remove `DQL_STRICT_MODE = false` workaround (now the v5 default)
- If your v4 app relied on `DQL_STRICT_MODE = true` (v4 default), you MUST set it before `sync.start()`:
  `ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = true")`

SYNC:
- `ditto.startSync()` → `ditto.sync.start()`
- `ditto.stopSync()` → `ditto.sync.stop()`
- `ditto.isSyncActive` → `ditto.sync.isActive`
- `ditto.sync.registerSubscription(query, args)` — args can be `Map<String, Any?>` (plain map) or `DittoCborSerializable.Dictionary`

AUTHENTICATION — CRITICAL SYNTAX RULES:
`auth.expirationHandler` is a Kotlin PROPERTY. You MUST use property assignment (`=`), NOT a method call.
There is NO `setExpirationHandler()` method — it does not exist and will not compile.
The lambda IS a suspend lambda — you call `auth.login()` directly inside it without launching a new coroutine.

CORRECT syntax:
```kotlin
ditto.auth?.let { auth ->
    auth.expirationHandler = { ditto, timeUntilExpiration ->
        // This lambda body IS suspend — call login() directly here, no coroutine needed
        ditto.auth?.login(token = "your-token", provider = DittoAuthenticationProvider.development())
    }
}
```

FORBIDDEN patterns that will NOT compile or will fail at runtime:
```kotlin
// ERROR: No such method exists
auth.setExpirationHandler { ditto, t -> ... }

// ERROR: Missing `=`; this calls the property as a function
auth.expirationHandler { ditto, t -> ... }

// WRONG: Never launch a new coroutine inside the handler — the lambda IS already suspend
auth.expirationHandler = { ditto, t ->
    CoroutineScope(Dispatchers.IO).launch { ditto.auth?.login(...) }
}
```

Other authentication rules:
- Handler receives `(ditto: Ditto, timeUntilExpiration: Double)` — note: param is now `Ditto`, not `DittoBase`
- Called when `timeUntilExpiration == 0.0` (initial auth or expired) or `> 0` (token expiring soon — refresh)
- `ditto.auth` is nullable — use `?.let { }` guard; it is null when using `DittoConfig.Connect.SmallPeersOnly`
- You MUST set `expirationHandler` BEFORE calling `ditto.sync.start()` when using `DittoConfig.Connect.Server`; failing to set it causes `DittoException.AuthenticationException` with reason `ExpirationHandlerMissing`
- Use `DittoAuthenticationProvider.development()` for playground tokens
- Use `DittoAuthenticationProvider.custom("provider-name")` for custom providers
- `auth.login(token, provider)` is now suspend — no callback parameter
- `auth.observeStatus()` returns `StateFlow<DittoAuthenticationStatus>` — was callback-based
- `auth.logout(cleanupFn: ((Ditto) -> Unit)?)` — param type changed from `DittoBase` to `Ditto`
- Remove `DittoAuthenticationCallback`, `DittoLoginCallback`, `DittoLoginCompletionCallback`, `DittoAuthenticationStatusDidChangeCallback`, `DittoLogoutCleanupFn` — all removed
- Remove `loginWithToken()`, `loginWithCredentials()` — deprecated and removed

STORE EXECUTE — CRITICAL BREAKING CHANGE FOR READS:
In v4, `store.execute(query, args)` returned `DittoQueryResult` directly.
In v5, `store.execute(query, args)` returns NOTHING (Unit). Calling `.items` on the result IS A COMPILE ERROR.

FORBIDDEN v4 pattern that will NOT compile in v5:
```kotlin
// ERROR: store.execute() returns Unit in v5, not DittoQueryResult
val item = store.execute("SELECT * FROM tasks WHERE _id = :id", mapOf("_id" to id)).items.first()
```

CORRECT v5 patterns for reads:
```kotlin
// Option A — handler form (RECOMMENDED): result is auto-closed after lambda returns
val item = ditto.store.execute(
    "SELECT * FROM tasks WHERE _id = :id",
    mapOf("_id" to id)
) { result -> result.items.firstOrNull() }

// Option B — executeRaw: same as v4 behavior, but YOU must close the result
val result = ditto.store.executeRaw("SELECT * FROM tasks WHERE _id = :id", mapOf("_id" to id))
val item = result.items.firstOrNull()
result.close()  // REQUIRED — resource leak if omitted
```

For WRITES (INSERT/UPDATE/DELETE), fire-and-forget is correct — no handler needed:
```kotlin
// Correct for writes — returns nothing, that's expected
ditto.store.execute("UPDATE tasks SET done = :done WHERE _id = :id", mapOf("done" to true, "id" to id))
```

Other store.execute rules:
- Replace `store.execute(query, args): DittoQueryResult` with new overloads:
  - Fire-and-forget (writes): `store.execute(query, args)` — discards result
  - With handler (reads): `store.execute(query, args) { result -> result.items.toList() }` — result auto-closed
  - Raw (manual): `store.executeRaw(query, args): DittoQueryResult` — caller must close
- All are `suspend` functions
- Arguments accept `Map<String, Any?>` (plain Kotlin map — recommended), `DittoCborSerializable.Dictionary`, or `@Serializable` types
- Remove `store.executeBlocking()` — use `runBlocking { store.execute() }` if needed
- `DittoQueryResultItem.value` is now `DittoCborSerializable.Dictionary` instead of `Map<String, Any>` — the cast `doc.value["field"] as SomeType` will throw at runtime; use typed accessors: `.stringOrNull`, `.intOrNull`, `.longOrNull`, `.doubleOrNull`, `.booleanOrNull`, `.listOrNull`, `.dictionaryOrNull`, `.attachmentTokenOrNull`, `.isNull`
- Or use `item.jsonString()` to get a JSON string and deserialize from JSON (recommended for complex objects)

OBSERVERS:
Three patterns replace the v4 callback model:

1. `registerObserver(query, args) { result -> ... }` — handler is now `suspend`; returns `DittoStoreObserver`; supports optional `DittoDiff` second parameter
2. `observe(query, args) { result -> transform }` — NEW; returns `Flow<T>`; auto-cancelled with scope; supports optional `DittoDiff`
3. `collect(query, args) { result -> ... }` — NEW; suspend function providing natural backpressure via coroutine suspension

- Replace `runOnUiThread { ... }` with `withContext(Dispatchers.Main) { ... }` inside suspend handlers
- Replace `DittoSignalNext` / `signalNext` backpressure with `collect()` method
- Remove `DittoChangeHandler`, `DittoChangeHandlerWithNextSignal`, `DittoSignalNext`, `DittoSignalNextCallback` — all removed
- When using `observe()` in `lifecycleScope`, observer is auto-cancelled — no manual `.close()` needed

LEGACY COLLECTION API — COMPLETELY REMOVED:
All collection-based query APIs are removed. Use DQL queries via `store.execute()` instead:
- `store.collection("x").find(query).exec()` → `store.execute("SELECT * FROM x WHERE ...", args) { result -> result.items }`
- `store.collection("x").upsert(value)` → `store.execute("INSERT INTO x DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE", mapOf("doc" to value))`
- `store.collection("x").findById(id).update { doc -> doc["field"].set(val) }` → `store.execute("UPDATE x SET field = :val WHERE _id = :id", mapOf("val" to val, "id" to id))`
- `store.collection("x").findById(id).remove()` → `store.execute("DELETE FROM x WHERE _id = :id", mapOf("id" to id))`
- `store.collection("x").findById(id).evict()` → `store.execute("EVICT FROM x WHERE _id = :id", mapOf("id" to id))`
- Counter: `doc["count"].counter.increment(1.0)` → `store.execute("UPDATE x APPLY count PN_INCREMENT BY :n WHERE _id = :id", mapOf("n" to 1, "id" to id))`
- `store.collection("x").find(q).observeLocal { docs, event -> ... }` → `store.observe(dqlQuery, args) { result -> ... }.collect { ... }`
- `store.collection("x").find(q).subscribe()` → `ditto.sync.registerSubscription(dqlQuery, args)`
- Remove: `DittoCollection`, `DittoDocument`, `DittoMutableDocument`, `DittoDocumentId`, `DittoDocumentPath`, `DittoDocumentIdPath`, `DittoMutableDocumentPath`, `DittoMutableCounter`, `DittoMutableRegister`, `DittoPendingCursorOperation`, `DittoPendingIdSpecificOperation`, `DittoPendingCollectionsOperation`, `DittoLiveQuery`, `DittoLiveQueryEvent`, `DittoSingleDocumentLiveQueryEvent`, `DittoSubscription` (legacy), `DittoWriteTransaction`, `DittoScopedWriteTransaction`, `DittoWriteTransactionResult`, `DittoUpdateResult`, `DittoWriteStrategy`, `DittoSortDirection`, `DittoCollectionsEvent`, `DittoLiveQueryMove`

TRANSACTIONS:
- `store.transaction { txn -> DittoTransactionCompletionAction.Commit }` → `store.transaction { txn -> DittoTransaction.Result.Commit(value) }` — now generic, returns a value
- `DittoTransactionCompletionAction.Abort` → `DittoTransaction.Result.Rollback`
- `store.write(block)` → `store.transaction { ... }` — write transactions removed
- Remove `transactionBlocking()`, `transactionReturningVoid()` — use `runBlocking { store.transaction {} }` if needed

TRANSPORT CONFIG:
Changed from mutable data classes to immutable builder pattern:
- `ditto.updateTransportConfig { config -> config.peerToPeer.bluetoothLe.enabled = true }` →
  `ditto.updateTransportConfig { config -> config.peerToPeer { bluetoothLe { enabled = true } } }`
- Builder uses DSL-style nested blocks
- `enableAllPeerToPeer()` — removed; configure each transport explicitly
- `DittoAdvertisementPower`: `VERY_LOW`→`VeryLow`, `LOW`→`Low`, `MEDIUM`→`Medium`, `HIGH`→`High`
- `DittoAdvertisementFrequency`: `LOW`→`Low`, `MEDIUM`→`Medium`, `HIGH`→`High`
- `DittoBluetoothLeConfig.maxOutgoing`, `.mtuRequest` — removed
- `DittoHttpListenConfig.staticContentPath` — removed
- `DittoGlobalConfig.syncGroupLong`, `.routingHintLong` — removed; use `UInt` directly
- Nested class renames: `DittoPeerToPeer`→`DittoTransportConfig.PeerToPeer`, `DittoBluetoothLeConfig`→`PeerToPeer.BluetoothLE`, `DittoLanConfig`→`PeerToPeer.Lan`, `DittoWifiAwareConfig`→`PeerToPeer.WifiAware`, `DittoConnect`→`Connect`, `DittoListen`→`Listen`, `DittoTcpListenConfig`→`Listen.TcpConfig`, `DittoHttpListenConfig`→`Listen.HttpConfig`, `DittoGlobalConfig`→`Global`

TRANSPORT CONDITION:
- `ditto.callback = object : DittoCallback { override fun transportConditionDidChange(...) }` →
  `ditto.transportCondition.collect { event -> /* event.condition, event.subsystem */ }`
- `ditto.transportCondition` is a `Flow<DittoTransportConditionEvent>`
- Remove `DittoCallback`, `DittoTransportConditionChangedCallback` — replaced by Flow

PRESENCE:
- `presence.observe(handler)` returning `DittoPresenceObserver` → `presence.observe()` returning `Flow<DittoPresenceGraph>` — collect in `lifecycleScope`
- `DittoPresenceGraph.json()` → `DittoPresenceGraph.serializeToJson()`
- `presence.peerMetadata: Map<String, Any?>` → `presence.peerMetadata: DittoJsonSerializable.ObjectValue` — use `getPeerMetadataMap()` for plain Map or `setPeerMetadata(Map)` to set
- `presence.connectionRequestHandlerJava` — removed; use `presence.connectionRequestHandler` fun interface
- `DittoConnectionRequest.peerKeyString` → `DittoConnectionRequest.peerKey`
- Remove `DittoPresenceObserver`, `DittoPresenceObserverCallback`, `DittoPeersObserver` — use Flow

PEER & CONNECTION:
- `DittoPeer.peerKeyString` → `DittoPeer.peerKey`
- `DittoPeer.peerKey: DittoPeerKey` (ByteArray) — removed; use `peerKey: String`
- `DittoPeer.isConnectedToDittoCloud` → `DittoPeer.isConnectedToDittoServer`
- `DittoPeer.os: String?` → `DittoPeer.os: DittoPeerOs?` (enum: `Generic`, `Ios`, `Tvos`, `Android`, `Linux`, `Windows`, `MacOS`)
- `DittoPeer.peerMetadata: Map<String, Any?>` → `DittoJsonSerializable.ObjectValue`
- `DittoPeer.identityServiceMetadata: Map<String, Any?>` → `DittoJsonSerializable.ObjectValue`
- `DittoPeer.queryOverlapGroup` — removed
- `DittoConnection.peer1/peer2` simplified to `String` (peer key); `approximateDistanceInMeters` removed
- Remove `DittoPeerKey` typealias, `toKeyString()`, `decodePeerKey()`, `DittoRemotePeer`

ATTACHMENTS:
- `store.fetchAttachment(token, callback)` → `suspend store.fetchAttachment(token, onFetchProgress?)` — now suspend, returns `DittoAttachmentFetchResult`
- `DittoAttachmentFetchEvent` (Progress/Completed/Deleted) → `DittoAttachmentFetchResult` (Completed/Deleted); progress via `onFetchProgress: suspend (ULong, ULong) -> Unit` parameter
- `store.newAttachment(path, metadata)` — metadata accepts `Map<String, Any?>` or `DittoCborSerializable.Dictionary` (changed from `Map<String, String>`)
- Remove `DittoAttachmentFetchEventType` enum, `DittoCollection.newAttachment()`, `DittoCollection.fetchAttachment()` (deprecated)

ERROR HANDLING:
- `DittoError` sealed class → `DittoException` sealed class
- All subclasses renamed: `DittoError.StoreError` → `DittoException.StoreException`, `AuthenticationError` → `AuthenticationException`, `FatalError` → `FatalException`, etc.
- All reason classes renamed: `StoreErrorReason` → `StoreExceptionReason`, `AuthenticationErrorReason` → `AuthenticationExceptionReason`, etc.
- Removed deprecated: `FilesystemError`, `InternalError`, `LockedWorkingDirectoryError`
- NEW exception types: `SmallPeerInfoException`, `DataStreamsException`, `DittoPanicException`

LOGGING:
- `DittoLogger.enabled` → `DittoLogger.isEnabled`
- `DittoLogLevel.DEBUG` → `DittoLogLevel.Debug` (PascalCase for all: `Error`, `Warning`, `Info`, `Debug`, `Verbose`)
- `DittoLogger.setCustomLogCallback(callback)` / `unsetCustomLogCallback()` → `DittoLogger.observeLogEvents(): Flow<DittoLogEvent>` — collect in coroutine scope
- `DittoLogger.exportToFileBlocking()` return type changed from `BigInteger` to `ULong`
- Remove `DittoLogger.emojiLogLevelHeadingsEnabled`, `DittoLogCallback` interface

DISK USAGE:
- `DiskUsage` → `DittoDiskUsage`; `DiskUsageItem` → `DittoDiskUsageItem`; `FileSystemType` → `DittoFileSystemType`
- `diskUsage.exec` → `diskUsage.item`
- `DiskUsageItem.sizeInBytes: Int` → `DittoDiskUsageItem.sizeInBytes: Long`
- `diskUsage.observe(handler): Closeable` → `diskUsage.observe(): Flow<DittoDiskUsageItem>`
- `FileSystemType.DIRECTORY/FILE/SYMLINK` → `DittoFileSystemType.Directory/File/Symlink`
- Remove `DiskUsageCallback` interface

OTHER DITTO CLASS CHANGES:
- `ditto.appId` → removed (use `DittoConfig.databaseId`)
- `ditto.siteId` → removed
- `ditto.persistenceDirectory` → removed (use `ditto.absolutePersistenceDirectory`)
- `ditto.sdkVersion` → removed (use `Ditto.VERSION` companion const)
- `ditto.activated` → `ditto.isActivated`
- `ditto.observePeers()` / `observePeersV2()` → removed (use `ditto.presence.observe()`)
- `ditto.smallPeerInfo.syncScope` → removed; `DittoSmallPeerInfoSyncScope` enum removed
- Classes implement `DittoResource` (extends `AutoCloseable`) instead of `java.io.Closeable`

BEFORE (V4):
```kotlin
import live.ditto.*
import live.ditto.android.*
import live.ditto.transports.*

// Initialize
val androidDeps = DefaultAndroidDittoDependencies(context)
val ditto = Ditto(
    androidDeps,
    DittoIdentity.OnlinePlayground(
        dependencies = androidDeps,
        appId = "my-app-id",
        token = "my-token",
        customAuthUrl = "https://my-app-id.cloud.ditto.live"
    )
)
ditto.updateTransportConfig { config ->
    config.connect.websocketUrls.add("wss://my-app-id.cloud.ditto.live")
}
ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = false")
ditto.disableSyncWithV3()
ditto.startSync()

// Register observer with callback
val observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    mapOf("color" to "red")
) { result ->
    runOnUiThread { updateUI(result.items) }
}

// Presence
val presenceObserver = ditto.presence.observe { graph ->
    graph.remotePeers.forEach { println(it.peerKeyString) }
}

// Transport condition
ditto.callback = object : DittoCallback {
    override fun transportConditionDidChange(condition, subsystem) {
        println("$subsystem: $condition")
    }
}

// Error handling
try {
    ditto.sync.start()
} catch (e: DittoError) {
    when (e) {
        is DittoError.StoreError -> println(e.reason)
        else -> println(e.message)
    }
}

// Cleanup
observer.close()
presenceObserver.close()
```

AFTER (V5):
```kotlin
import com.ditto.kotlin.*
import com.ditto.kotlin.transports.*
import com.ditto.kotlin.serialization.*

// Initialize
val ditto = DittoFactory.create(
    DittoConfig(
        databaseId = "my-app-id",
        connect = DittoConfig.Connect.Server("https://my-app-id.cloud.ditto.live"),
    )
)

// Authentication
ditto.auth?.let { auth ->
    auth.expirationHandler = { dittoInstance, timeUntilExpiration ->
        dittoInstance.auth?.login(
            token = "my-token",
            provider = DittoAuthenticationProvider.development(),
        )
    }
}

ditto.sync.start()

// Register observer — handler is now suspend
val observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    mapOf("color" to "red")
) { result ->
    withContext(Dispatchers.Main) { updateUI(result.items) }
}

// OR use observe() Flow — auto-cancelled with lifecycleScope
lifecycleScope.launch {
    ditto.store.observe(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "red")
    ) { result ->
        result.items.map { it.jsonString() }
    }.collect { cars ->
        updateUI(cars)
    }
}

// Presence — Flow-based
lifecycleScope.launch {
    ditto.presence.observe().collect { graph ->
        graph.remotePeers.forEach { println(it.peerKey) }
    }
}

// Transport condition — Flow-based
lifecycleScope.launch {
    ditto.transportCondition.collect { event ->
        println("${event.subsystem}: ${event.condition}")
    }
}

// Error handling
try {
    ditto.sync.start()
} catch (e: DittoException) {
    when (e) {
        is DittoException.StoreException -> println(e.reason)
        else -> println(e.message)
    }
}

// Cleanup — observer still needs manual close if not using Flow
observer.close()
// Flow-based observers auto-cancel with lifecycleScope — no cleanup needed
```

DQL MIGRATION EXAMPLES:
```kotlin
// Query — v4: store.collection("cars").find("color == 'blue'").exec()
val cars = ditto.store.execute(
    "SELECT * FROM cars WHERE color = :color",
    mapOf("color" to "blue")
) { result -> result.items.toList() }

// Insert — v4: store.collection("cars").upsert(mapOf("_id" to id, "color" to "blue"))
ditto.store.execute(
    "INSERT INTO cars DOCUMENTS (:doc) ON ID CONFLICT DO UPDATE",
    mapOf("doc" to mapOf("_id" to id, "color" to "blue"))
)

// Update — v4: store.collection("cars").findById(id).update { doc -> doc["color"].set("green") }
ditto.store.execute(
    "UPDATE cars SET color = :color WHERE _id = :id",
    mapOf("color" to "green", "id" to id)
)

// Delete — v4: store.collection("cars").findById(id).remove()
ditto.store.execute(
    "DELETE FROM cars WHERE _id = :id",
    mapOf("id" to id)
)

// Evict — v4: store.collection("cars").findById(id).evict()
ditto.store.execute(
    "EVICT FROM cars WHERE _id = :id",
    mapOf("id" to id)
)

// Counter — v4: doc["viewCount"].counter.increment(1.0)
ditto.store.execute(
    "UPDATE cars APPLY viewCount PN_INCREMENT BY :n WHERE _id = :id",
    mapOf("n" to 1, "id" to id)
)

// Subscribe — v4: store.collection("cars").find("color == 'red'").subscribe()
val sub = ditto.sync.registerSubscription(
    "SELECT * FROM cars WHERE color = :color",
    mapOf("color" to "red")
)
```

CHECKLIST:
- Replace ALL `live.ditto.*` imports with `com.ditto.kotlin.*`; add `serialization.*` and `transports.*` imports
- Replace `Ditto(dependencies, identity)` / `Ditto.open()` / `Ditto.openSync()` with `DittoFactory.create(DittoConfig(...))`
- Remove `DefaultAndroidDittoDependencies`, `DittoIdentity`, `DittoAndroidConfig` usage
- Replace `DittoConfigConnect.Server(URI(...))` with `DittoConfig.Connect.Server("url")`
- Replace `startSync()` / `stopSync()` with `sync.start()` / `sync.stop()`
- Set `auth.expirationHandler` suspend lambda for authentication
- Replace `DittoAuthenticationCallback` interface with expiration handler
- Replace callback-based `login()` with `suspend auth.login(token, provider)`
- Replace collection-based queries with DQL via `store.execute()`
- Update `store.execute()` calls: use handler for reads, fire-and-forget for writes, `executeRaw()` for manual lifecycle
- Convert observer callbacks to suspend handlers, or use `observe()` Flow, or `collect()` for backpressure
- Replace `runOnUiThread` with `withContext(Dispatchers.Main)` in suspend handlers
- Update transport config to builder DSL pattern
- Replace `ditto.callback` with `ditto.transportCondition` Flow
- Replace `presence.observe(handler)` with `presence.observe()` Flow
- Replace `DittoError` catches with `DittoException`; update all subclass names (`StoreError` → `StoreException`, etc.)
- Replace `peerKeyString` with `peerKey`; `isConnectedToDittoCloud` with `isConnectedToDittoServer`
- Replace callback-based `fetchAttachment()` with suspend version returning `DittoAttachmentFetchResult`
- Rename `DiskUsage` → `DittoDiskUsage`, `DiskUsageItem` → `DittoDiskUsageItem`; update `diskUsage.exec` → `diskUsage.item`
- Update `DittoLogger.enabled` → `.isEnabled`; `DittoLogLevel.DEBUG` → `.Debug`
- Replace `setCustomLogCallback()` with `observeLogEvents()` Flow
- Remove `DittoTransportConfig.enableAllPeerToPeer()` — configure each transport explicitly
- Remove `disableSyncWithV3()`, `runGarbageCollection()`, `getTransportDiagnostics()`
- Set `DQL_STRICT_MODE = true` BEFORE `sync.start()` if your v4 app relied on the v4 default
- Replace `store.write(block)` with `store.transaction { DittoTransaction.Result.Commit(value) }`
- Update `DittoAttachment.metadata` / `DittoAttachmentToken.metadata` type from `Map<String, String>` to `Map<String, Any?>` (or `DittoCborSerializable.Dictionary`)

MANDATORY FINAL STEP — BUILD AND FIX ALL ERRORS:
After completing ALL code changes above, you MUST attempt to build the project and fix every compilation error before declaring the migration complete.

Run: `./gradlew assembleDebug` (or build in Android Studio)

Do NOT stop when the IDE shows no red underlines — actually build the project. Fix every error before finishing.

Most common build errors and their fixes:
- `Unresolved reference: live.ditto` → missed an import or usage of a v4 class; replace with `com.ditto.kotlin`
- `Unresolved reference: DittoSyncPermissions` → it moved to `com.ditto.kotlin.transports`; update import to `import com.ditto.kotlin.transports.DittoSyncPermissions`
- `Unresolved reference: items` on a `store.execute()` call → you used fire-and-forget (returns Unit); switch to handler form: `store.execute(query, args) { result -> result.items.toList() }`
- `Type mismatch: inferred type is Unit but X was expected` on store.execute result → same fix as above
- `Unresolved reference: setExpirationHandler` → use property assignment: `auth.expirationHandler = { ditto, t -> ... }`
- `Suspension functions can be called only within coroutine body` inside expirationHandler → the lambda IS already suspend; do not wrap in `launch {}`; call `auth.login()` directly
- `Unresolved reference: DittoError` → replace with `DittoException`
- `Unresolved reference: startSync` / `stopSync` / `isSyncActive` → replace with `sync.start()` / `sync.stop()` / `sync.isActive`
- `Unresolved reference: DefaultAndroidDittoDependencies` → remove entirely; no replacement needed
- Cast `doc.value["field"] as SomeType` throws `ClassCastException` at runtime → use `item.jsonString()` + JSON deserialization, or typed accessors (`.stringOrNull`, `.intOrNull`, etc.)
- `error: <identifier> expected` in generated BuildConfig.java with double-quoted values like `""value""` → this is a pre-existing project issue where your .env file uses shell-style quoted values (`KEY="value"`) that Java's `Properties.load()` preserves literally; fix by stripping surrounding quotes from property values before passing them to `BuildConfigField`, e.g.: `val rawValue = prop[key]?.toString()?.trim('"') ?: ""`

Work through the codebase systematically. Show me each file's changes and confirm a successful build at the end.

Package Rename

All imports from live.ditto.* have changed to com.ditto.kotlin.* You must replace your definition in your gradle file:
ditto-kotlin = { module = "com.ditto:ditto-kotlin-android", version.ref = "ditto" }
  • live.ditto.*com.ditto.kotlin.*
  • live.ditto.transports.*com.ditto.kotlin.transports.*
  • live.ditto.android.*com.ditto.kotlin.* (Android-specific classes merged into main package)
  • Use IDE Find & Replace: live.dittocom.ditto.kotlin project-wide

Minimum Supported Android Version

Ditto SDK v5 requires minSdk = 24 (Android 7.0 Nougat). If your project targets minSdk = 23, you must raise it.

Ditto Instance Initialization

v5 separates initialization into four distinct phases for better clarity and control:
// 1. Configure
val config = DittoConfig(
    databaseId = "your-database-id",
    connect = DittoConfig.Connect.Server(
        url = "https://your-server.ditto.live"
    )
)

// 2. Initialize
val ditto = DittoFactory.create(config)

// 3. Authenticate
ditto.auth?.let { auth ->
    auth.expirationHandler = { ditto, timeUntilExpiration ->
        ditto.auth?.login(
            token = "your-token",
            provider = DittoAuthenticationProvider.development()
        )
    }
}

// 4. Start sync
ditto.sync.start()
These changes provide:
  • Clear separation of connection, initialization, and authentication concerns
  • No workarounds needed (transport config, DQL strict mode, v3 sync disable)
  • Simpler package structure (com.ditto.kotlin.* instead of live.ditto.*)
Package change: Update all imports from live.ditto.* to com.ditto.kotlin.* Initialization Migration Steps
1

Replace Initialization

Replace Ditto() constructor with DittoFactory.create(config).
import com.ditto.kotlin.*

val ditto = DittoFactory.create(
  DittoConfig(
    databaseId = "your-database-id",
    connect = DittoConfig.Connect.Server("https://your-server.ditto.live")
  )
)

// Set up auth handler before starting sync (see Step 2)
ditto.sync.start()
Key changes:
  • Update imports: live.ditto.*com.ditto.kotlin.*
  • Use DittoConfig instead of DittoIdentity
  • appIddatabaseId
  • DittoIdentity.OnlinePlaygroundDittoConfig.Connect.Server(url = "...")
  • DittoIdentity.OfflinePlaygroundDittoConfig.Connect.SmallPeersOnly(privateKey = null)
  • DittoFactory.create() is not suspend — no coroutine needed
  • Remove updateTransportConfig, disableSyncWithV3(), and DQL strict mode queries
  • Remove DefaultAndroidDittoDependencies parameter
2

Update Authentication

Set authentication handler separately after initialization.
// Set handler after DittoFactory.create()
ditto.auth?.let { auth ->
    auth.expirationHandler = { ditto, timeUntilExpiration ->
        // Initial auth or token refresh
        ditto.auth?.login(
            token = "your-token",
            provider = DittoAuthenticationProvider.development()
        )
    }
}
Key changes:
  • Authentication now uses expirationHandler property (suspend lambda)
  • Handler called when auth required (timeUntilExpiration == 0.0) or token expiring
  • Use DittoAuthenticationProvider.development() for playground tokens
  • Custom auth: DittoAuthenticationProvider.custom("your-provider")

Additional Changes

Observer Changes

v5 offers three observer patterns. The registerObserver() handler is now a suspend lambda. The new observe() returns a Flow<T>, and collect() provides natural backpressure.
// registerObserver returns DittoStoreObserver, handler is now suspend
val observer = ditto.store.registerObserver(
    "SELECT * FROM cars WHERE color = :color",
    mapOf("color" to "red")
) { result ->
    withContext(Dispatchers.Main) {
        cars = result.items
    }
}

// Still requires manual cleanup if not using Flow
observer.close()
Key changes:
  • registerObserver() handler is now suspend; still returns DittoStoreObserver (requires manual .close())
  • New observe() returns Flow<T> with a transform lambda — auto-cancelled with lifecycleScope
  • New collect() is a suspend function providing natural backpressure
  • Wrap Flow-based observers in lifecycleScope.launch { } for lifecycle-aware cancellation
  • Replace runOnUiThread with withContext(Dispatchers.Main)

Back Pressure

In Kotlin v5, back pressure is handled automatically through coroutine suspension. The registerObserver handler is a suspend lambda — while it is suspended or executing, Ditto will not deliver the next event. When the lambda returns, Ditto is automatically signaled to produce the next update.
Coalescing behavior: Changes that occur while your handler is executing are coalesced. When the handler completes and Ditto delivers the next callback, you receive the latest state — not every intermediate change.
// Suspension provides backpressure automatically
lifecycleScope.launch {
    ditto.store.collect(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "blue")
    ) { result ->
        // While this block is executing (or suspended) 
        // no new events are delivered
        expensiveOperation(result.items)
        withContext(Dispatchers.Main) { updateUI(result.items) }
        // When this lambda returns, Ditto delivers the next event
    }
}
For maximum throughput (accepting all updates as fast as possible), use the Flow-returning observe():
lifecycleScope.launch {
    ditto.store.observe(
        "SELECT * FROM cars WHERE color = :color",
        mapOf("color" to "blue")
    ) { result ->
        result.items
    }.collect { items ->
        // Flow signals immediately after emit 
        // use this when you want all events
        updateUI(items)
    }
}

Serialization

v5 adds inline reified overloads that accept @Serializable types:
@Serializable
data class CarArgs(val color: String)

// No .toDittoCbor() needed -- serialized automatically
ditto.store.execute(
    "SELECT * FROM cars WHERE color = :color",
    CarArgs(color = "red")
)

DQL Strict Mode Behavior Change

Breaking Change: v5 defaults to DQL_STRICT_MODE=false, which fundamentally changes how DQL queries behave.
  • v4 default: Objects treated as REGISTER (whole-object replacement)
  • v5 default: Objects treated as MAP (field-level merging)
This affects the behavior of all DQL SELECT, INSERT, and UPDATE operations.
Choose the appropriate migration path based on your current v4 configuration:
If you’re currently using the v4 default (DQL_STRICT_MODE=true), you must explicitly set strict mode to true in v5 before starting sync or executing any queries.
Failing to set strict mode will cause objects to merge at the field level instead of replacing entirely, which can result in unexpected data behavior and perceived data loss.
To migrate to DQL_STRICT_MODE=false (the new v5 default), contact Ditto Customer Support for guidance.
    val ditto = DittoFactory.create(
        DittoConfig(
            databaseId = "your-database-id",
            connect = DittoConfig.Connect.Server("https://your-server.ditto.live"),
        )
    )
    ditto.store.execute("ALTER SYSTEM SET DQL_STRICT_MODE = true")
    ditto.sync.start()
If you explicitly set DQL_STRICT_MODE=false in v4, no changes are required.v5 uses DQL_STRICT_MODE=false as the default, so your existing DQL queries will behave identically. You can upgrade freely.Optional: You can remove the explicit ALTER SYSTEM SET DQL_STRICT_MODE = false statement in v5 since this is now the default behavior.
val config = DittoConfig(
    databaseId = "your-database-id",
    connect = DittoConfig.Connect.Server(url = "https://your-server.ditto.live")
)
val ditto = DittoFactory.create(config)

// No need to set DQL_STRICT_MODE - false is now the default
ditto.sync.start()
For additional guidance or questions, contact Ditto Customer Support.

Default Persistence Directory

v5 includes the database ID in the default directory name: ditto-{databaseId} instead of ditto. This is for only new databases created in v5. v5 does not migrate existing databases to the new structure. You only need to set the persistence directory if you provided a custom directory in v4.
To maintain v4 compatibility:
val ditto = DittoFactory.create(
    DittoConfig(
        databaseId = "your-database-id",
        connect = DittoConfig.Connect.Server("https://your-server.ditto.live"),
        persistenceDirectory = context.filesDir.resolve("ditto").absolutePath
    )
)

API Renames

Areav4.14v5.0
Packagelive.ditto.*com.ditto.kotlin.*
InitDitto(deps, DittoIdentity.OnlinePlayground(...))DittoFactory.create(DittoConfig(...))
ConfigDittoAndroidConfig(context, databaseId, connect, ...)DittoConfig(databaseId, connect, ...) — no Context param; Android context handled internally via DittoInitializer
ConnectDittoConfigConnect.Server(url: URI)DittoConfig.Connect.Server(url: String)
Syncditto.startSync() / stopSync()ditto.sync.start() / sync.stop()
AuthDittoAuthenticationCallback interfaceauth.expirationHandler = { ditto, timeUntilExpiration -> ... } suspend lambda
ObserversCallback-based registerObserver() returns DittoStoreObserverregisterObserver() with suspend handler returns DittoStoreObserver, or observe() returning Flow, or collect() for backpressure
ErrorsDittoError sealed classDittoException sealed class
ParamsMap<String, Any?> with plain Kotlin valuesMap<String, Any?>, @Serializable types, or DittoCborSerializable.Dictionary
TransportMutable DittoTransportConfig data classDittoTransportConfig modified via updateTransportConfig() builder DSL
ResultsDittoQueryResultItem.value: Map<String, Any>DittoQueryResultItem.value: DittoCborSerializable.Dictionary
CloseableClasses implement java.io.CloseableClasses implement DittoResource (extends AutoCloseable)
Presencepresence.observe(handler) returns DittoPresenceObserverpresence.observe() returns Flow<DittoPresenceGraph>
DiskUsageDiskUsage, DiskUsageItem, FileSystemTypeDittoDiskUsage, DittoDiskUsageItem, DittoFileSystemType
PeerspeerKeyString, isConnectedToDittoCloudpeerKey, isConnectedToDittoServer

Key Initialization Changes

v4.14v5.0Notes
Ditto(dependencies, identity) constructorRemovedUse DittoFactory.create()
Ditto.open(DittoAndroidConfig) suspendRemovedUse DittoFactory.create() (not suspend)
Ditto.openSync(DittoAndroidConfig)RemovedUse DittoFactory.create() (not suspend)
DittoAndroidConfig(context, databaseId, connect, ...)RemovedUse DittoConfig(databaseId, connect, ...) — no Context parameter
DittoConfigConnect.Server(url: URI)DittoConfig.Connect.Server(url: String)URL is now a String, not java.net.URI; nested inside DittoConfig
DittoConfigConnect.SmallPeersOnly(privateKey?)DittoConfig.Connect.SmallPeersOnly(privateKey?)Now nested inside DittoConfig
DittoConfigExperimental(coreExceptionBridgingEnabled)DittoConfig.Experimental(coreExceptionBridgingEnabled)Now nested inside DittoConfig
DittoConfig properties are var (mutable)DittoConfig properties are val (immutable)Config is now immutable after construction
DittoConfig.persistenceDirectory: File?DittoConfig.persistenceDirectory: String?Changed from File to String
DefaultAndroidDittoDependencies(context)RemovedAndroid context is handled internally by DittoInitializer (AndroidX Startup)
AndroidDittoDependencies interfaceRemovedNo dependency injection needed
DittoDependencies interfaceRemovedNo dependency injection needed
DefaultDittoDependencies(baseDir)RemovedNo dependency injection needed
DittoBase abstract classRemovedDitto is the single concrete class

Identity Type Mapping

v4.14 DittoIdentityv5.0 Equivalent
OnlinePlayground(deps, appId, token, ...)DittoConfig.Connect.Server(url) + auth.expirationHandler + auth.login(token, DittoAuthenticationProvider.development())
OnlineWithAuthentication(deps, appId, callback, ...)DittoConfig.Connect.Server(url) + auth.expirationHandler + custom auth logic
OfflinePlayground(deps, appId)DittoConfig.Connect.SmallPeersOnly() + ditto.setOfflineOnlyLicenseToken(token)
SharedKey(deps, appId, sharedKey)DittoConfig.Connect.SmallPeersOnly(privateKey = sharedKey)
Manual(certificateConfig)Removed — no direct equivalent

Authentication API Changes

v4.14v5.0Notes
DittoAuthenticationCallback interfaceRemovedUse auth.expirationHandler suspend lambda
authenticator.loginWithToken(token, provider, callback)Removed (deprecated in v4)Use auth.login(token, provider) suspend
authenticator.loginWithCredentials(username, password, provider, callback)Removed (deprecated in v4)No direct replacement
auth.login(token, provider, completion: (String?, DittoError?) -> Unit)RemovedUse auth.login(token, provider) suspend (returns String?)
DittoLoginCallback interfaceRemoved
DittoLoginCompletionCallback interfaceRemoved
DittoAuthenticationStatusDidChangeCallback interfaceRemovedUse auth.observeStatus() Flow
DittoLogoutCleanupFn interfaceRemovedUse lambda directly
auth.observeStatus(callback: (DittoAuthenticationStatus) -> Unit): Closeableauth.observeStatus(): StateFlow<DittoAuthenticationStatus>Returns StateFlow instead of Closeable
auth.logout(cleanupFn: ((DittoBase) -> Unit)?)auth.logout(cleanupFn: ((Ditto) -> Unit)?)Parameter type changed from DittoBase to Ditto
auth.expirationHandler: suspend (DittoBase, Double) -> Unitauth.expirationHandler: suspend (Ditto, Double) -> UnitParameter type changed from DittoBase to Ditto

Key Store API Changes

v4.14v5.0Notes
store.execute(query, arguments): DittoQueryResultstore.execute(query, arguments) (discards result) or store.execute(query, args) { handler } (auto-close)Use executeRaw() if you need the old behavior
store.executeBlocking(query, arguments)RemovedUse runBlocking { store.execute(...) } if needed
store.collection(name)RemovedUse DQL queries with store.execute()
store[name] operatorRemovedUse DQL queries
store.collectionNames()RemovedUse store.execute("SELECT DISTINCT collection FROM __collections")
store.collections()RemovedUse DQL queries
store.write(block)RemovedUse store.transaction()
store.queriesHash() / queriesHashMnemonic()Removed

Observer Migration Guide

v4.14 Patternv5.0 Replacement
registerObserver(query, args) { result -> ... } callbackregisterObserver(query, args) { result -> ... } (handler is now suspend)
registerObserver + DittoSignalNext for backpressurecollect() — backpressure via coroutine suspension
Manual observer.close() in onDestroy()Same pattern, or use observe().collect {} in lifecycleScope (auto-cancelled)
runOnUiThread { ... } inside handlerwithContext(Dispatchers.Main) { ... } inside suspend handler

Removed Observer Types

v4.14v5.0Notes
DittoChangeHandler typealiasRemovedHandler is now suspend (DittoQueryResult) -> Unit
DittoChangeHandlerWithNextSignal typealiasRemovedUse collect() for backpressure
DittoSignalNext / DittoSignalNextCallbackRemovedBackpressure via coroutine suspension in collect()

Transaction Changes

v4.14v5.0Notes
transaction { txn -> DittoTransactionCompletionAction.Commit }transaction { txn -> DittoTransaction.Result.Commit(value) }Now generic, returns value
DittoTransactionCompletionAction.AbortDittoTransaction.Result.RollbackRenamed
transactionBlocking()RemovedUse runBlocking { store.transaction {} }
transactionReturningVoid()RemovedUse transaction { DittoTransaction.Result.Commit() }
txn.execute(query, Map<String, Any?>?)txn.execute(query, DittoCborSerializable.Dictionary) or Map<String, Any?>Same overloads as store.execute()
txn.executeRaw() (NEW)Returns raw DittoQueryResult for manual lifecycle

Transport Config Changes

v4.14v5.0Notes
DittoTransportConfig mutable data classDittoTransportConfig immutable class with Builder
DittoPeerToPeer data classDittoTransportConfig.PeerToPeerNested class
DittoBluetoothLeConfig data classDittoTransportConfig.PeerToPeer.BluetoothLENested class
DittoBluetoothLeConfig.maxOutgoingRemoved
DittoBluetoothLeConfig.mtuRequestRemoved
DittoLanConfig data classDittoTransportConfig.PeerToPeer.LanNested class
DittoWifiAwareConfig data classDittoTransportConfig.PeerToPeer.WifiAwareNested class
DittoConnect data classDittoTransportConfig.ConnectNested class
DittoListen data classDittoTransportConfig.ListenNested class
DittoTcpListenConfigDittoTransportConfig.Listen.TcpConfigNested class
DittoHttpListenConfigDittoTransportConfig.Listen.HttpConfigNested class
DittoHttpListenConfig.staticContentPathRemoved
DittoGlobalConfig data classDittoTransportConfig.GlobalNested class
DittoGlobalConfig.syncGroupLong / routingHintLong (Java compat)RemovedUse UInt directly
enableAllPeerToPeer()RemovedConfigure each transport explicitly
DittoAdvertisementPower enum: VERY_LOW, LOW, MEDIUM, HIGHVeryLow, Low, Medium, HighPascalCase enum values
DittoAdvertisementFrequency enum: LOW, MEDIUM, HIGHLow, Medium, HighPascalCase enum values
ditto.transportConfig get/setditto.transportConfig get/set; prefer updateTransportConfig()updateTransportConfig() builder DSL is the recommended approach

Presence API Changes

v4.14v5.0Notes
presence.observe(handler) returns DittoPresenceObserverpresence.observe() returns Flow<DittoPresenceGraph>Flow-based; auto-cancelled with scope
DittoPresenceObserver interfaceRemovedUse Flow cancellation
DittoPresenceObserverCallback interfaceRemovedUse Flow
DittoPresenceGraph.json()DittoPresenceGraph.serializeToJson()Method renamed
presence.peerMetadata: Map<String, Any?> (get/set)presence.peerMetadata: DittoJsonSerializable.ObjectValue (get/set)Type changed; use getPeerMetadataMap() for plain Map
presence.getPeerMetadataMap(): Map<String, Any?> (NEW)Convenience method
presence.setPeerMetadata(Map<String, Any?>) (NEW)Convenience method
presence.peerMetadataJsonBytes: ByteArray (NEW)Binary JSON representation
presence.connectionRequestHandler: DittoConnectionRequestHandler? (typealias)presence.connectionRequestHandler: DittoPresence.ConnectionRequestHandler? (fun interface)
presence.connectionRequestHandlerJava: DittoConnectionRequestHandlerCallback?RemovedUse the fun interface directly
DittoConnectionRequest.peerKeyStringDittoConnectionRequest.peerKeyProperty renamed
DittoConnectionRequest.peerMetadata: Map<String, Any?>DittoConnectionRequest.peerMetadata: DittoJsonSerializable.ObjectValueType changed
DittoConnectionRequest.identityServiceMetadata: Map<String, Any?>DittoConnectionRequest.identityServiceMetadata: DittoJsonSerializable.ObjectValueType changed
Presence.peerMetadataMaxSizeInBytes companion constRemoved from public API

DittoPeer Changes

v4.14v5.0Notes
peerKeyString: StringpeerKey: StringRenamed
peerKey: DittoPeerKey (ByteArray)RemovedUse peerKey: String
DittoPeerKey typealias (ByteArray)Removed
DittoPeerKey.toKeyString() extensionRemoved
String.decodePeerKey() extensionRemoved
isConnectedToDittoCloud: BooleanisConnectedToDittoServer: BooleanRenamed
os: String?os: DittoPeerOs?Now an enum: Generic, Ios, Tvos, Android, Linux, Windows, MacOS
peerMetadata: Map<String, Any?>peerMetadata: DittoJsonSerializable.ObjectValueType changed
identityServiceMetadata: Map<String, Any?>identityServiceMetadata: DittoJsonSerializable.ObjectValueType changed
queryOverlapGroup: Int (deprecated)Removed

DittoConnection Changes

v4.14v5.0Notes
peer1: DittoAddress (deprecated) / peerKeyString1: Stringpeer1: StringSimplified to peer key string
peer2: DittoAddress (deprecated) / peerKeyString2: Stringpeer2: StringSimplified to peer key string
approximateDistanceInMeters: Double?Removed

DittoAddress Changes

v4.14v5.0Notes
DittoAddress(siteId, pubkey) data classDittoAddress opaque classNo public properties beyond toString(), equals(), hashCode()
DittoAddress.siteId (deprecated)Removed

Attachment API Changes

v4.14v5.0Notes
store.newAttachment(path, Map<String, String>)store.newAttachment(path, Map<String, Any?>)Metadata type widened; also accepts DittoCborSerializable.Dictionary
store.newAttachmentBlocking(inputStream, Map<String, String>)store.newAttachmentBlocking(inputStream, DittoCborSerializable.Dictionary)Only CBOR overload for blocking variant
store.fetchAttachment(token, callback) returns DittoAttachmentFetchersuspend store.fetchAttachment(token, onFetchProgress?) returns DittoAttachmentFetchResultNow suspend; returns result
DittoAttachmentFetchEvent sealed class (Progress/Completed/Deleted)DittoAttachmentFetchResult sealed class (Completed/Deleted)Progress handled via onFetchProgress callback
DittoAttachmentFetchEvent.Progress(downloadedBytes, totalBytes)onFetchProgress: suspend (ULong, ULong) -> Unit parameterSeparate progress callback
DittoAttachmentFetchEventType enumRemovedNot needed with sealed result class
DittoAttachmentFetcher (manual lifecycle)Still exists but store.fetchAttachment() is preferredTracked in store.attachmentFetchers (NEW)
DittoCollection.newAttachment() (deprecated)RemovedUse store.newAttachment()
DittoCollection.fetchAttachment() (deprecated)RemovedUse store.fetchAttachment()

Transport Condition Changes

Transport Condition (Replaces ditto.callback)

// v5
lifecycleScope.launch {
    ditto.transportCondition.collect { event ->
        // event.transportCondition, event.conditionSource
    }
}
v4.14v5.0Notes
ditto.callback: DittoCallback?ditto.transportCondition: Flow<DittoTransportConditionEvent>Flow-based
DittoCallback interfaceRemoved
DittoTransportConditionChangedCallback interfaceRemoved
DittoTransportCondition + DittoConditionSource separate paramsDittoTransportConditionEvent(condition, subsystem)Wrapped in event class
DittoTransportCondition.Source nested enum: Bluetooth, Tcp, Awdl, MdnsSubsystem enum

Logging Changes

v4.14v5.0Notes
DittoLogger.enabled: BooleanDittoLogger.isEnabled: BooleanRenamed
DittoLogger.emojiLogLevelHeadingsEnabledRemoved
DittoLogger.setCustomLogCallback(callback)RemovedUse observeLogEvents()
DittoLogger.unsetCustomLogCallback()RemovedCancel the Flow scope
DittoLogger.setCustomLogCallback(DittoLogCallback)Removed
DittoLogCallback interfaceRemoved
DittoLogger.observeLogEvents(): Flow<DittoLogEvent> (NEW)Returns Flow of log events
DittoLogger.DittoLogEvent(level, message) (NEW)Log event data class
DittoLogger.exportToFileBlocking(): BigIntegerDittoLogger.exportToFileBlocking(path: String): ULongReturn type changed; now requires path parameter. Also available: suspend exportToFile(path: String): ULong
DittoLogLevel.ERROR, WARNING, INFO, DEBUG, VERBOSEDittoLogLevel.Error, Warning, Info, Debug, VerbosePascalCase enum values
DittoLog.e/w/i/d/v() methodsSame signature in v5No change
DittoLogDecorator interfaceSame in v5No change

Disk Usage Changes

v4.14v5.0Notes
DiskUsage classDittoDiskUsage classRenamed with Ditto prefix
DiskUsageItem data classDittoDiskUsageItem classRenamed
FileSystemType enum: DIRECTORY, FILE, SYMLINKDittoFileSystemType enum: Directory, File, SymlinkRenamed; PascalCase values
diskUsage.exec: DiskUsageItemdiskUsage.item: DittoDiskUsageItemProperty renamed
DiskUsageItem.sizeInBytes: IntDittoDiskUsageItem.sizeInBytes: LongWidened from Int to Long
diskUsage.observe(handler): CloseablediskUsage.observe(): Flow<DittoDiskUsageItem>Flow-based
DiskUsageCallback interfaceRemovedUse Flow

Error Handling

// v5 — DittoError renamed to DittoException
try {
    ditto.sync.start()
} catch (e: DittoException) {
    println("Error: ${e.message}")
}

Exception Hierarchy Changes

v4.14 DittoErrorv5.0 DittoExceptionNotes
ActivationError(reason)ActivationException(reason)Renamed; reasons: same naming
AuthenticationError(reason)AuthenticationException(reason)Renamed
FatalError(message)FatalExceptionRenamed
FilesystemError(message) (deprecated)RemovedWas deprecated in v4
InternalError(reason) (deprecated)RemovedWas deprecated in v4
IoError(reason)IoException(reason)Renamed
LockedWorkingDirectoryError(message) (deprecated)RemovedWas deprecated in v4; use StoreException(PersistenceDirectoryLocked)
PresenceError(reason)PresenceException(reason)Renamed
StoreError(reason)StoreException(reason)Renamed
TransportError(reason)TransportException(reason)Renamed
UnknownError(message)UnknownExceptionRenamed
UnsupportedError(message)UnsupportedExceptionRenamed
ValidationError(reason)ValidationException(reason)Renamed
SmallPeerInfoException(reason) (NEW)For small peer info errors
DataStreamsException (NEW)For Data Streams API errors
DittoPanicException (NEW)For native crashes; has nativeCrashMessage and nativeCrashStackTrace

Reason Classes Renamed

All *ErrorReason sealed classes are renamed to *ExceptionReason:
  • ActivationErrorReason -> ActivationExceptionReason
  • AuthenticationErrorReason -> AuthenticationExceptionReason
  • IoErrorReason -> IoExceptionReason
  • PresenceErrorReason -> PresenceExceptionReason
  • StoreErrorReason -> StoreExceptionReason
  • TransportErrorReason -> TransportExceptionReason
  • ValidationErrorReason -> ValidationExceptionReason
  • NEW: SmallPeerInfoExceptionReason

Specific Reason Changes

v4.14v5.0Notes
PresenceErrorReason.JsonParsingError(message)PresenceExceptionReason.FailedToEncodeJson(cause)Renamed and param type changed
TransportErrorReason.FailedToDecodeTransportDiagnosticsRemoved
StoreErrorReason.FailedToEncodeValueStoreExceptionReason.FailedToEncodeValue(value, message, cause)Additional params
StoreExceptionReason.AttachmentDataRetrievalError (NEW)
StoreExceptionReason.AttachmentFileCopyError (NEW)
StoreExceptionReason.CrdtError (NEW)
StoreExceptionReason.DocumentContentEncodingFailed (NEW)
StoreExceptionReason.FailedToDecodeData(cause, data) (NEW)
StoreExceptionReason.FailedToGetDocumentData(path) (NEW)
StoreExceptionReason.FailedToGetDocumentIDData(path) (NEW)
StoreExceptionReason.InvalidDocumentStructure(cbor) (NEW)
StoreExceptionReason.NonStringKeyInDocument(key) (NEW)
SmallPeerInfoExceptionReason.SizeLimitExceeded (NEW)
SmallPeerInfoExceptionReason.DepthLimitExceeded (NEW)
SmallPeerInfoExceptionReason.NotADictionary (NEW)
ValidationErrorReason.UnknownSyncScopeRemoved
Update ALL catch (e: DittoError) blocks to catch (e: DittoException).

Removed APIs

These v4 classes, interfaces, and APIs have been completely removed in v5

Classes Removed

  • DittoCollection — use DQL queries via store.execute()
  • DittoDocument / DittoMutableDocument — results are now DittoQueryResultItem; use item.jsonString() or typed accessors on item.value
  • DittoDocumentId — use _id field in DQL queries
  • DittoDocumentPath / DittoDocumentIdPath — access values via typed accessors or jsonString()
  • DittoMutableDocumentPath — use UPDATE SET DQL
  • DittoMutableCounter / DittoMutableRegister — use DQL PN_INCREMENT / SET
  • DittoCounter / DittoRegister (property accessors on document paths)
  • DittoPendingCursorOperation — use DQL queries
  • DittoPendingIdSpecificOperation — use DQL queries
  • DittoPendingCollectionsOperation — use DQL queries
  • DittoLiveQuery — use registerObserver(), observe(), or collect()
  • DittoLiveQueryEvent sealed class (Initial, Update) — use DittoDiff
  • DittoSingleDocumentLiveQueryEvent — use DittoDiff
  • DittoLiveQueryMove — use DittoDiff.Move
  • DittoSubscription (legacy) — use DittoSyncSubscription via sync.registerSubscription()
  • DittoWriteTransaction / DittoScopedWriteTransaction — use store.transaction()
  • DittoWriteTransactionPendingCursorOperation / DittoWriteTransactionPendingIdSpecificOperation
  • DittoWriteTransactionResult sealed class (Inserted, Updated, Evicted, Removed) — use DittoQueryResult.mutatedDocumentIds()
  • DittoUpdateResult sealed class (Set, Removed, Incremented)
  • DittoCollectionsEvent
  • DittoRemotePeer
  • DittoTransportDiagnostics

Enums Removed

  • DittoWriteStrategy (Merge, InsertIfAbsent, InsertDefaultIfAbsent, UpdateDifferentValues) — use DQL ON ID CONFLICT clauses
  • DittoSortDirection (Ascending, Descending) — use DQL ORDER BY ... ASC/DESC
  • DittoConnectionPriority (DontConnect, Normal, High)
  • DittoSmallPeerInfoSyncScope (BigPeerOnly, LocalPeerOnly)
  • DittoBase64PaddingMode
  • DittoAttachmentFetchEventType (Completed, Progress, Deleted)

Interfaces Removed

  • DittoCallback / DittoTransportConditionChangedCallback — use transportCondition Flow
  • DittoAuthenticationCallback — use auth.expirationHandler suspend lambda
  • DittoLoginCallback / DittoLoginCompletionCallback
  • DittoAuthenticationStatusDidChangeCallback — use auth.observeStatus() StateFlow
  • DittoLogoutCleanupFn — use lambda
  • DittoConnectionRequestHandlerCallback — use DittoPresence.ConnectionRequestHandler fun interface
  • DittoPeersObserver / DittoPeersObserverV1Callback / DittoPeersObserverV2Callback
  • DittoPresenceObserver — use Flow
  • DittoPresenceObserverCallback
  • DittoLiveQueryCallback / DittoLiveQueryWithNextSignalCallback
  • DittoSingleDocumentLiveQueryCallback / DittoSingleDocumentLiveQueryWithNextSignalCallback
  • DittoMutableDocumentsUpdater / DittoSingleMutableDocumentUpdater
  • DittoWriteTransactionHandler
  • DiskUsageCallback
  • DittoSignalNextCallback
  • DittoLogCallback
  • DittoDependencies / DefaultDittoDependencies
  • AndroidDittoDependencies / DefaultAndroidDittoDependencies
  • JavaDittoDependencies / DefaultJavaDittoDependencies

Type Aliases Removed

  • DittoChangeHandler
  • DittoChangeHandlerWithNextSignal
  • DittoSignalNext
  • DittoConnectionRequestHandler (typealias; replaced by DittoPresence.ConnectionRequestHandler fun interface)
  • DittoAuthenticationExpirationHandler / DittoAuthenticationExpirationHandlerSync (the handler is still used but type is inline)
  • DittoPeerKey

New APIs in v5

  • DittoFactory.create(DittoConfig(...)) — new initialization via config object (no Context parameter; Android context handled internally via DittoInitializer)
  • DittoConfig.Connect.Server / DittoConfig.Connect.SmallPeersOnly — connect mode types nested inside DittoConfig
  • DittoException — replaces DittoError as the sealed exception hierarchy
  • ditto.sync sub-object with start(), stop(), isActive, registerSubscription()
  • ditto.transportCondition: Flow<DittoTransportConditionEvent> — replaces ditto.callback
  • auth.observeStatus(): StateFlow — observe authentication status as a Flow
  • store.registerObserver() — now accepts a suspend handler; returns DittoStoreObserver
  • store.observe() — new Flow-based observer returning Flow<T> with a transform lambda
  • store.collect() — new suspend function providing natural backpressure via coroutine suspension
  • presence.observe() — now returns Flow<DittoPresenceGraph> (was callback-based)
  • Data Streams API (preview) — streaming data subscription model
  • updateTransportConfig() — preferred builder DSL for modifying transport config

Migration Checklist

Initialization

  • Update imports: live.ditto.*com.ditto.kotlin.*
  • Replace Ditto(deps, DittoIdentity.OnlinePlayground(...)) with DittoFactory.create(DittoConfig(...))
  • Create DittoConfig with databaseId and connect mode
  • Update DittoIdentity.OnlinePlaygroundDittoConfig.Connect.Server(url = "...")
  • Update DittoIdentity.OfflinePlaygroundDittoConfig.Connect.SmallPeersOnly(privateKey = null)
  • Remove DefaultAndroidDittoDependencies parameter
  • Remove updateTransportConfig calls
  • Remove disableSyncWithV3() calls
  • Set DQL_STRICT_MODE=true BEFORE starting sync if maintaining v4 behavior
  • Update startSync()sync.start()

Observers

  • Convert callback observers to suspend handlers or Flow-based observe()/collect()
  • Wrap observers in lifecycleScope.launch { }
  • Replace runOnUiThread with withContext(Dispatchers.Main)
  • Remove manual observer .close() calls when using Flow-based observers
  • Remove observer property tracking

Authentication

  • Set expirationHandler property on auth after initialization
  • Use DittoAuthenticationProvider.development() for playground tokens
  • Use DittoAuthenticationProvider.custom("provider") for custom auth providers
  • Remove authentication from identity configuration

Query Arguments

  • Use Map<String, Any?> for DQL query arguments (recommended)
  • Alternatively, use @Serializable data classes for type-safe arguments
  • Apply to INSERT, UPDATE, SELECT arguments

Breaking Changes

  • Set persistenceDirectory if maintaining v4 directory structure
  • Update peerKeyStringpeerKey
  • Update ALL catch (e: DittoError) to catch (e: DittoException)
  • Replace ditto.callback with ditto.transportCondition: Flow<...>
  • Update isConnectedToDittoCloudisConnectedToDittoServer

Verification

  • Build compiles with zero errors
  • No deprecated API warnings
  • Observers update UI immediately
  • Authentication works before sync starts
  • No ClassCastException in serialization
  • No memory leaks on Fragment navigation