Skip to main content

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.

Package Rename

All imports from live.ditto.* have changed to com.ditto.kotlin.* You must replace your definition in your gradle file:
  • Use the KMP root artifact com.ditto:ditto-kotlin (the same coordinate the install guide and the v5 tools use). For an Android app, Gradle reads the module metadata and automatically resolves the platform-specific -android variant — you do not need to write com.ditto:ditto-kotlin-android yourself.
  • 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:
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).
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.
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. registerObserver() and collect() invoke a suspend handler; observe() returns a cold Flow<T> whose transform converts each result into plain data before emission.
Query-result lifetime — do not return DittoQueryResult or its items from a handler/transform. A DittoQueryResult and the DittoQueryResultItems it owns are native resources whose lifetime is bound to the observer’s current event. As soon as your handler returns (or the next event arrives), they are closed. Reaching back into result.items from outside the lambda — or storing them in a var/StateFlow — will throw IllegalStateException: Query result item has already been closed.Always extract the data you need (call item.jsonString(), item.value["..."].string, etc.) inside the lambda and return plain Kotlin objects.
Key changes:
  • registerObserver() handler is now suspend; still returns DittoStoreObserver (manual .close()).
  • New observe() returns Flow<T> with a transform lambda — cancelled with the collecting scope (e.g. lifecycleScope).
  • New collect() is a suspending function — Ditto only signals the next event after your handler returns, giving you back-pressure via coroutine suspension.
  • Replace runOnUiThread with withContext(Dispatchers.Main).
  • Need diffs? registerObserver, collect, and observe each have an overload whose lambda takes a second DittoDiff parameter — e.g. registerObserver(query, args) { result, diff -> ... }. DittoDiff exposes insertions, deletions, and updates (each a Set<Int> of indices into the result list) plus moves: List<DittoDiff.Move>.
  • v4’s standalone com.ditto.kotlin.DittoDiffer() is no longer user-constructible — the class survives with an internal constructor and its lifecycle is now owned by the observer. If you were computing diffs manually via DittoDiffer().diff(result.items), switch to the (result, diff) observer overload above; the observer creates and closes the differ for you, which also closes a native-memory leak footgun that existed in v4 when startObserving orphaned the previous differ (see ditto#21533).
  • v4’s registerObserver(query, args) { result, signalNext -> } back-pressure overload is removed — there is no signalNext parameter in v5. Back-pressure is now automatic via the suspend handler (see Back-pressure below); migrate any manual signalNext logic to the suspend/collect() model.

Back-pressure

registerObserver() and collect() provide back-pressure automatically: while their suspend handler is running or suspended, Ditto coalesces changes and only delivers the next event once your handler returns.
Coalescing: changes that occur while your handler is running are merged. When the next callback fires you receive the latest state, not every intermediate change.
observe() does not provide natural back-pressure. The returned Flow signals Ditto to produce the next event right after emit succeeds, so a slow collector will not slow down the producer — you may receive emissions faster than your collector can drain them. If you need rendezvous-style back-pressure on the Flow side, add an explicit operator:
For UI snapshots (you only care about the latest state), .conflate() is usually the right choice. For ordered processing where every event matters, prefer collect() over observe() — the suspend handler model is the only API that natively rate-limits the producer to your handler’s pace.

Serialization

v5 adds inline reified overloads that accept @Serializable types:

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.
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.Recommended: Remove the explicit ALTER SYSTEM SET DQL_STRICT_MODE = false statement entirely — the v5 default already matches your intent, so the line is now redundant.
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:

API Renames

Key Initialization Changes

Identity Type Mapping

Authentication API Changes

Key Store API Changes

Observer Migration Guide

Removed Observer Types

Transaction Changes

Transport Config Changes

Presence API Changes

DittoPeer Changes

DittoConnection Changes

DittoAddress Changes

Attachment API Changes

Reading attachment bytes: byte access on a DittoAttachment is exposed as functions, not Kotlin properties. Use attachment.getInputStream().readBytes() or attachment.getData() (returns ByteArray). attachment.data and attachment.inputStream do not resolve as properties and will not compile.

Transport Condition Changes

Transport Condition (Replaces ditto.callback)

Logging Changes

Disk Usage Changes

Error Handling

Exception Hierarchy Changes

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

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