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.Copy AI Migration Prompt (Click to Expand)
Copy AI Migration Prompt (Click to Expand)
Package Rename
All imports fromlive.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-androidvariant — you do not need to writecom.ditto:ditto-kotlin-androidyourself. 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.ditto→com.ditto.kotlinproject-wide
Minimum Supported Android Version
Ditto SDK v5 requiresminSdk = 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:- 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 oflive.ditto.*)
live.ditto.* to com.ditto.kotlin.*
Initialization Migration Steps
1
Replace Initialization
Replace Key changes:
Ditto() constructor with DittoFactory.create(config).- Update imports:
live.ditto.*→com.ditto.kotlin.* - Use
DittoConfiginstead ofDittoIdentity appId→databaseIdDittoIdentity.OnlinePlayground→DittoConfig.Connect.Server(url = "...")DittoIdentity.OfflinePlayground→DittoConfig.Connect.SmallPeersOnly(privateKey = null)DittoFactory.create()is not suspend — no coroutine needed- Remove
updateTransportConfig,disableSyncWithV3(), and DQL strict mode queries - Remove
DefaultAndroidDittoDependenciesparameter
2
Update Authentication
Set authentication handler separately after initialization.Key changes:
- Authentication now uses
expirationHandlerproperty (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.
registerObserver()handler is nowsuspend; still returnsDittoStoreObserver(manual.close()).- New
observe()returnsFlow<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
runOnUiThreadwithwithContext(Dispatchers.Main). - Need diffs?
registerObserver,collect, andobserveeach have an overload whose lambda takes a secondDittoDiffparameter — e.g.registerObserver(query, args) { result, diff -> ... }.DittoDiffexposesinsertions,deletions, andupdates(each aSet<Int>of indices into the result list) plusmoves: 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 viaDittoDiffer().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 whenstartObservingorphaned the previous differ (see ditto#21533). - v4’s
registerObserver(query, args) { result, signalNext -> }back-pressure overload is removed — there is nosignalNextparameter in v5. Back-pressure is now automatic via thesuspendhandler (see Back-pressure below); migrate any manualsignalNextlogic to thesuspend/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:
.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
Choose the appropriate migration path based on your current v4 configuration:Currently Using DQL with DQL_STRICT_MODE=true (v4 default)
Currently Using DQL with DQL_STRICT_MODE=true (v4 default)
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.To migrate to
DQL_STRICT_MODE=false (the new v5 default), contact Ditto Customer Support for guidance.Currently Using DQL with DQL_STRICT_MODE=false
Currently Using DQL with DQL_STRICT_MODE=false
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.Default Persistence Directory
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->ActivationExceptionReasonAuthenticationErrorReason->AuthenticationExceptionReasonIoErrorReason->IoExceptionReasonPresenceErrorReason->PresenceExceptionReasonStoreErrorReason->StoreExceptionReasonTransportErrorReason->TransportExceptionReasonValidationErrorReason->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 v5Classes Removed
DittoCollection— use DQL queries viastore.execute()DittoDocument/DittoMutableDocument— results are nowDittoQueryResultItem; useitem.jsonString()or typed accessors onitem.valueDittoDocumentId— use_idfield in DQL queriesDittoDocumentPath/DittoDocumentIdPath— access values via typed accessors orjsonString()DittoMutableDocumentPath— useUPDATE SETDQLDittoMutableCounter/DittoMutableRegister— use DQLPN_INCREMENT/SETDittoCounter/DittoRegister(property accessors on document paths)DittoPendingCursorOperation— use DQL queriesDittoPendingIdSpecificOperation— use DQL queriesDittoPendingCollectionsOperation— use DQL queriesDittoLiveQuery— useregisterObserver(),observe(), orcollect()DittoLiveQueryEventsealed class (Initial,Update) — useDittoDiffDittoSingleDocumentLiveQueryEvent— useDittoDiffDittoLiveQueryMove— useDittoDiff.MoveDittoSubscription(legacy) — useDittoSyncSubscriptionviasync.registerSubscription()DittoWriteTransaction/DittoScopedWriteTransaction— usestore.transaction()DittoWriteTransactionPendingCursorOperation/DittoWriteTransactionPendingIdSpecificOperationDittoWriteTransactionResultsealed class (Inserted,Updated,Evicted,Removed) — useDittoQueryResult.mutatedDocumentIds()DittoUpdateResultsealed class (Set,Removed,Incremented)DittoCollectionsEventDittoRemotePeerDittoTransportDiagnostics
Enums Removed
DittoWriteStrategy(Merge,InsertIfAbsent,InsertDefaultIfAbsent,UpdateDifferentValues) — use DQLON ID CONFLICTclausesDittoSortDirection(Ascending,Descending) — use DQLORDER BY ... ASC/DESCDittoConnectionPriority(DontConnect,Normal,High)DittoSmallPeerInfoSyncScope(BigPeerOnly,LocalPeerOnly)DittoBase64PaddingModeDittoAttachmentFetchEventType(Completed,Progress,Deleted)
Interfaces Removed
DittoCallback/DittoTransportConditionChangedCallback— usetransportConditionFlowDittoAuthenticationCallback— useauth.expirationHandlersuspend lambdaDittoLoginCallback/DittoLoginCompletionCallbackDittoAuthenticationStatusDidChangeCallback— useauth.observeStatus()StateFlowDittoLogoutCleanupFn— use lambdaDittoConnectionRequestHandlerCallback— useDittoPresence.ConnectionRequestHandlerfun interfaceDittoPeersObserver/DittoPeersObserverV1Callback/DittoPeersObserverV2CallbackDittoPresenceObserver— use FlowDittoPresenceObserverCallbackDittoLiveQueryCallback/DittoLiveQueryWithNextSignalCallbackDittoSingleDocumentLiveQueryCallback/DittoSingleDocumentLiveQueryWithNextSignalCallbackDittoMutableDocumentsUpdater/DittoSingleMutableDocumentUpdaterDittoWriteTransactionHandlerDiskUsageCallbackDittoSignalNextCallbackDittoLogCallbackDittoDependencies/DefaultDittoDependenciesAndroidDittoDependencies/DefaultAndroidDittoDependenciesJavaDittoDependencies/DefaultJavaDittoDependencies
Type Aliases Removed
DittoChangeHandlerDittoChangeHandlerWithNextSignalDittoSignalNextDittoConnectionRequestHandler(typealias; replaced byDittoPresence.ConnectionRequestHandlerfun 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 viaDittoInitializer)DittoConfig.Connect.Server/DittoConfig.Connect.SmallPeersOnly— connect mode types nested insideDittoConfigDittoException— replacesDittoErroras the sealed exception hierarchyditto.syncsub-object withstart(),stop(),isActive,registerSubscription()ditto.transportCondition: Flow<DittoTransportConditionEvent>— replacesditto.callbackauth.observeStatus(): StateFlow— observe authentication status as a Flowstore.registerObserver()— now accepts a suspend handler; returnsDittoStoreObserverstore.observe()— new Flow-based observer returningFlow<T>with a transform lambdastore.collect()— new suspend function providing natural backpressure via coroutine suspensionpresence.observe()— now returnsFlow<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(...))withDittoFactory.create(DittoConfig(...)) - Create
DittoConfigwithdatabaseIdandconnectmode - Update
DittoIdentity.OnlinePlayground→DittoConfig.Connect.Server(url = "...") - Update
DittoIdentity.OfflinePlayground→DittoConfig.Connect.SmallPeersOnly(privateKey = null) - Remove
DefaultAndroidDittoDependenciesparameter - Remove
updateTransportConfigcalls - Remove
disableSyncWithV3()calls - Set
DQL_STRICT_MODE=trueBEFORE 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
runOnUiThreadwithwithContext(Dispatchers.Main) - Remove manual observer
.close()calls when using Flow-based observers - Remove observer property tracking
Authentication
- Set
expirationHandlerproperty onauthafter 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
@Serializabledata classes for type-safe arguments - Apply to INSERT, UPDATE, SELECT arguments
Breaking Changes
- Set
persistenceDirectoryif maintaining v4 directory structure - Update
peerKeyString→peerKey - Update ALL
catch (e: DittoError)tocatch (e: DittoException) - Replace
ditto.callbackwithditto.transportCondition: Flow<...> - Update
isConnectedToDittoCloud→isConnectedToDittoServer
Verification
- Build compiles with zero errors
- No deprecated API warnings
- Observers update UI immediately
- Authentication works before sync starts
- No
ClassCastExceptionin serialization - No memory leaks on Fragment navigation