Share
Offline should be the norm: building local-first apps with CRDTs & Kotlin Multiplatform
In 2026, a loading spinner in the subway isn’t “bad connectivity”. It’s a cloud-first app showing its limits. This talk is about offline-first architecture: write locally first, and treat sync as a background process, not a user-blocking gate.
The hard part is concurrency: Alice edits a to-do offline while Bob checks it off. Who wins? Definitely not “last-write-wins”. We will unpack CRDTs, a data structure designed to merge changes without conflicts and converge reliably. Then we go hands-on with Kotlin Multiplatform by designing a practical local-first architecture, local persistence, a sync engine built around deltas, and the real-world challenges (tombstones, pruning, UI “glitches”). The goal: one robust, shared sync core for Android, iOS, the Web, and apps that stays trustworthy even when the network isn’t.
Renaud MATHIEU
Transcript
65 slides
-
DROIDCON LISBON 2026 Offline should be the norm Building local-first
apps with CRDTs and Kotlin Multiplatform Renaud Mathieu - renaudmathieu.com Paris Android User Group - https://mobilemakers.fr/en/ 20 OCTOBRE 2026 · @RENAUDMATHIEU.COM -
IN THE MARKET, NO NETWORK What is displayed on the
app? 1. Please connect to the internet and retry? 2. “Pastel de nata” or “Gluten free Pastel de nata”? 3. Are all the items checked or unchecked? -
PART 01 / 09 01 Principles of offline-first What it
means for an app to work without a network. -
01 · PRINCIPLES Why offline-first? Users encounter network lapses daily
because of: Limited bandwidth Interruptions Infrequent access In areas with poor coverage. In elevators or tunnels. Wi-Fi-only tablets, rural areas, travel. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
01 · PRINCIPLES What is an offline-first application? An offline-first
app can operate without internet, with or without some features. Specifically, it must fulfill three criteria: 01 02 03 Remain usable without a reliable network connection. Present local data immediately, without waiting for a first network call. Retrieve data in a battery and dataconscious way (e.g. only sync on Wi-Fi or while charging). OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
01 · PRINCIPLES The central role of the data layer
All offline-first thinking starts in the data layer of the app architecture. This layer provides access to application data and business logic. The two fundamental operations on data are: 1. Reads 2. Writes Retrieve data to display to the user. Persist user input to retrieve later. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
PART 02 / 09 02 Data layer Local sources, network
and mapping. -
02 · DATA LAYER App architecture UI layer Domain layer
(optional) Data layer OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER App architecture UI layer Domain layer
(optional) Data layer Repositories Data sources OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER The two data sources An offline-first
app has at minimum two data sources for each repository that uses network resources: LOCAL NETWORK The local data source The network data source OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER The local source: the source of
truth The local source is the source of truth (single source of truth). It must be the exclusive source of all data read by the upper layers. This guarantees data consistency across connection states. COMMON LOCAL PERSISTENCE Structured data Unstructured data Simple files Relational databases like Room. Protocol Buffers with DataStore. JSON, XML, etc. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER The network source: the real state
The network source represents the real state of the application on the server side. The local source is at best synchronized with it. It can be behind in both directions: Network ahead Local ahead The app must update itself when it comes back online. The network must be updated when connectivity returns. The domain and UI layers must never communicate directly with the network source. That is the responsibility of the repository. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER · MODEL 1 OF 3, NETWORK
Separate models per layer Each data source often needs its own representation. We therefore define three models: @Serializable data class NetworkAuthor( val id: String, val name: String, val imageUrl: String, val twitter: String, val mediumPage: String, val bio: String, ) OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER · MODEL 2 OF 3, LOCAL
Separate models per layer @Entity(tableName = "authors") data class AuthorEntity( @PrimaryKey val id: String, val name: String, @ColumnInfo(name = "image_url") val imageUrl: String, @ColumnInfo(defaultValue = "") val twitter: String, @ColumnInfo(name = "medium_page", defaultValue = "") val mediumPage: String, @ColumnInfo(defaultValue = "") val bio: String, ) OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER · MODEL 3 OF 3, EXTERNAL
Separate models per layer Each data source often needs its own representation. We therefore define three models: data class Author( val id: String, val name: String, val imageUrl: String, val twitter: String, val mediumPage: String, val bio: String, ) OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
Mapping functions Conversions between models are done via extension functions:
Network → Local fun NetworkAuthor.asEntity() = AuthorEntity( id = id, name = name, imageUrl = imageUrl, twitter = twitter, mediumPage = mediumPage, bio = bio, ) Local → External fun AuthorEntity.asExternalModel() = Author( id = id, name = name, imageUrl = imageUrl, twitter = twitter, mediumPage = mediumPage, bio = bio, ) / OFFLINE-FIRST / / / 02 · DATA LAYER 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
02 · DATA LAYER Why three models? This separation protects
the external layers from minor changes in the local or network sources that do not fundamentally change the app's behavior. For example, a column rename in the database, or a change in the JSON format, does not affect the UI. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
PART 03 / 09 03 Synchronization Pull or push: two
ways to catch up with the server. -
03 · SYNCHRONIZATION Two synchronization models OFFLINE-FIRST 1 Basics When
an offline-first app regains connectivity, it must reconcile its local data with the network's. This process is called synchronization. 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
03 · SYNCHRONIZATION Pull-based synchronization The app contacts the network
to read the latest data on demand. The common heuristic is navigationbased: the app only fetches data just before presenting it. Strengths Trade-offs • Relatively easy to implement • Prone to high data consumption (unnecessary re- • Unnecessary data is never fetched fetches) • Not well suited for relational data OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
03 · SYNCHRONIZATION Push-based synchronization The local source attempts to
replicate the network source as closely as possible. It proactively fetches an appropriate volume of data on first launch, then relies on server notifications to know when data is stale. Strengths Trade-offs • The app can stay offline indefinitely • Versioning for conflict resolution is non-trivial • Minimal data consumption (only fetches changes) • The network source must support synchronization • Works well for relational data OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
04 · REACTIVE READS Reads via observable types The key
pattern: read APIs expose Flow<T>. The repository reads directly from the local source and can only notify its consumers of changes by first updating the local source. class OfflineFirstTopicsRepository( private val topicDao: TopicDao, private val network: NiaNetworkDataSource, ) : TopicsRepository { override fun getTopicsStream(): Flow<List<Topic = topicDao .getTopicEntitiesStream() .map { it.map(TopicEntity asExternalModel) } } 2 Data > : : 1 Basics > OFFLINE-FIRST 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
04 · REACTIVE READS Data flow Reads in an offline-first
repository must read directly from the local source. Updates are written to the local source first, which then notifies its consumers because it is observable. 01 02 The ViewModel collects the Flow The repository exposes the from the repository. Flow from the local DAO. OFFLINE-FIRST 1 Basics 2 Data 03 04 When the network updates the local DB, the Flow emits automatically. The UI updates accordingly. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
04 · ERROR HANDLING Error handling: local source Read errors
from the local source are rare but possible. We protect against them with the catch operator on Flow: class AuthorViewModel( authorsRepository: AuthorsRepository, ) : ViewModel() { private val authorId: String = private val authorStream: Flow<Author> = authorsRepository .getAuthorStream(id = authorId) .catch { emit(Author.empty()) } } OFFLINE-FIRST . . . 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
04 · ERROR HANDLING Error handling: network source If errors
occur when reading from the network, the app should use a retry heuristic. STRATEGY 1 Exponential backoff The app attempts to read from the network at increasing intervals until success or a stop condition is reached. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
04 · ERROR HANDLING Error handling: network source If errors
occur when reading from the network, the app should use a retry heuristic. STRATEGY 2 Network connectivity monitoring Read requests are queued until the app is certain it can connect. Once the connection is established, the queue is drained, data is read, and the local source is updated. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
04 · ERROR HANDLING Error handling: network source Combining both
strategies. In practice, both approaches are often combined: 01 02 Network monitoring to trigger synchronization at the right moment. Exponential backoff to handle temporary failures during synchronization. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
PART 05 / 09 05 Offline write strategies Online-only, queued
or lazy, and the conflicts that follow. -
05 · WRITES Offline write strategies If the recommended method
for reading data is to use observable types, the equivalent for write APIs is to use asynchronous APIs, such as suspend functions in Kotlin. This avoids blocking the UI thread and simplifies error handling, since offline-first writes can fail when crossing a network boundary. interface UserDataRepository { suspend fun updateNewsResourceBookmark( newsResourceId: String, bookmarked: Boolean ) } OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
05 · WRITES · STRATEGY 1 OF 3 Online-only writing
The app attempts to write data via the network. On success, it updates the local source. On failure, it throws an exception. Use case Handling failure Transactions that must happen online and in near real time. Example: a bank transfer. • Hide the write UI if the app requires internet to write (disable buttons). • Popup message or transient prompt to inform the user they are offline. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
05 · WRITES · STRATEGY 2 OF 3 Queued writes
The object to write is inserted into a queue. The queue is drained with exponential backoff when the app comes back online. Use case: a good choice when • It is not essential that the data is written to the network. • The transaction is not time-sensitive. • It is not essential to inform the user of failure. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
05 · WRITES · STRATEGY 3 OF 3 Lazy writes
The app writes to the local source first, then queues the write to notify the network as soon as possible. This is the most complex strategy, as it can cause conflicts between local and network sources when the app comes back online. Use case Watch out Good choice when the data is critical for the app. Example: in an offline-first to-do list app, tasks added offline must be stored locally to avoid data loss. OFFLINE-FIRST 1 Basics 2 Data 3 Sync Writing data in offline-first apps often requires more thought than reading, due to potential conflicts. An offlinefirst app does not need to be able to write offline to be considered offline-first. 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
05 · WRITES Conflict resolution OFFLINE-FIRST 1 Basics 2 Data
If the app writes data locally when offline and that data is misaligned with the network source, a conflict arises. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
05 · CONFLICT RESOLUTION Last write wins This is the
most common approach for mobile applications: 01 02 03 04 Each device attaches a timestamp to written data. The network source receives data from the different devices. It rejects data older than its current state. It accepts more recent data. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
PART 06 / 09 06 Mathematical foundations of CRDTs Why
convergence is guaranteed by construction. -
CRDTs (Conflict-free Replicated Data Types) rest on precise algebraic properties
that guarantee convergence by construction. 06 · FOUNDATIONS Mathematical foundations of CRDTs OFFLINE-FIRST Before writing any code, it is essential to understand the underlying mathematics. 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
06 · FOUNDATIONS Partial order A partial order is a
binary relation ≤ on a set S that satisfies three properties: PROPERTY DEFINITION Reflexivity ∀x ∈ S, x ≤ x Antisymmetry ∀x, y ∈ S, (x ≤ y ∧ y ≤ x) Transitivity ∀x, y, z ∈ S, (x ≤ y ∧ y ≤ z) x=y x≤z Why it ma ers. Phone A holds {Pastel de Nata}, phone B holds {Potatoes}. Neither contains the other, so neither is “newer”: they are concurrent, and a total order would force us to pick one. tt OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
06 · FOUNDATIONS Join-semilattice A join-semilattice is a partially ordered
set (S, ≤) in which every pair of elements x, y has a least upper bound (also called join), denoted x ⊔ y. Definition of the least upper bound x ⊔ y is the smallest element z such that: •x ≤ z •y ≤ z • ∀w, (x ≤ w ∧ y ≤ w) ⇒ z ≤ w OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
06 · FOUNDATIONS Join-semilattice Properties of the join operation ⊔
(merge). The operation ⊔ must be: Commutative Associative Idempotent x ⊔ y = y ⊔ x (x ⊔ y) ⊔ z = x ⊔ (y ⊔ z) x ⊔ x = x The order in which states are received does not matter. Receiving the same state twice changes nothing. You can merge in groups in any order. {Onions} ⊔ {Potatoes} = {Onions, Potatoes} OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
06 · FOUNDATIONS Monotonicity of updates For the system to
work, updates must be monotone: each update only “moves up” in the partial order. Formally, if s is the current state and f an update operation: s ≤ f(s) This means you can never go back. Information only grows. This is why elements like tombstones (deletion markers) are necessary: you do not “delete”, you add the information that an element was deleted. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
06 · FOUNDATIONS Strong eventual consistency The central property of
CRDTs is Strong Eventual Consistency (SEC). If: IF THEN Two replicas have received the same set of updates (in any order), They are in the same state. This is stronger than classic eventual consistency, because no manual conflict resolution is needed. Convergence is automatic and guaranteed by the algebraic structure. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
06 · FOUNDATIONS The complete logical chain 01 02 03
04 05 Partial order Semilattice Monotonicity Strong convergence Models incomparable concurrent states. Guarantees that a least upper bound (merge) always exists. Commutativity, associativity, idempotence Updates only “move up”. Same updates, same final state, guaranteed. OFFLINE-FIRST 1 Basics Invariance to reordering and duplication. 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
07 · TAXONOMY · PRINCIPLE State-based CRDTs (CvRDTs) CvRDTs (“Convergent
Replicated Data Types”) send their complete state to other replicas. The receiving replica merges the received state with its local state via the merge function. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
07 · TAXONOMY · REQUIREMENTS State-based CRDTs (CvRDTs) 01 02
03 The set of states must form a monotone join-semilattice. The merge function computes the least upper bound (LUB). Updates must be monotone (the state only grows). OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
07 · TAXONOMY · PRINCIPLE Delta-state CRDTs Delta-CRDTs are an
optimization of CvRDTs. Instead of sending the full state, only the recent changes (deltas) are sent. This combines the advantages of both approaches: FROM CVRDTS FROM CMRDTS Conceptual simplicity (semilattice, merge). Network efficiency (only transmit what changed). OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
07 · TAXONOMY · HOW IT WORKS Delta-state CRDTs 01
02 03 Each update produces a delta Deltas are accumulated in a (state fragment). buffer. OFFLINE-FIRST 1 Basics 2 Data 04 Periodically, the buffer is sent The receiver merges the delta to other replicas. with its local state via the same merge. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
07 · TAXONOMY Catalogue of common CRDTs G-Counter G-Set A
grow-only counter. Each replica counts its own increments, and the total is the sum of all replicas. Use it for things like view counts or likes. A grow-only set: you can add elements, but never remove them. Perfect for append-only logs or event streams. OR-Set LWW-Register Add and remove, with add-wins on conflict. Last write wins. Use it for simple fields. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
PART 08 / 09 08 Practical implementation Local-first with CRDTs
and Kotlin Multiplatform. -
08 · IMPLEMENTATION Why Kotlin Multiplatform? The synchronization logic (CRDTs,
merge, delta engine) is the most complex and critical part of the app. Duplicating it for each platform would be risky. KMP allows you to: Share Test once Isolate The sync engine between Android, iOS and the Web. The convergence logic, only once. The complexity inside a commonMain module. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
08 · IMPLEMENTATION · SYNCHRONIZATION FLOW The delta sync engine
ON THE DEVICE ON THE SERVER, THEN OTHER CLIENTS 1. The user makes a local modification. 1. The server merges the delta into its state. 2. The CRDT produces a delta (minimal state fragment). 2. The server broadcasts the deltas to other clients. 3. The delta is added to the pending delta buffer. 3. Clients receive and merge the remote deltas. 4. When the network is available, the buffer is sent to the server. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
08 · REAL-WORLD CHALLENGES Tombstones PROBLEM SOLUTION To support deletion
in an OR-Set, each deleted element leaves a tombstone. Over time, tombstones accumulate and consume memory and bandwidth. When we're sure all replicas have acknowledged a tombstone, it can be safely removed (pruning). OFFLINE-FIRST 1 Basics 2 Data Vector clocks help determine this: a tombstone is eligible for deletion if its timestamp is less than or equal to the local vector clock, indicating all replicas have processed it. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
08 · REAL-WORLD CHALLENGES UI glitches Problem. When a remote
merge arrives, the UI may briefly display inconsistent intermediate states. For example, a deleted element may reappear for a fraction of a second before being removed. A B C Update batching DiffUtil / ListDiffer Optimistic UI Accumulate incoming deltas and apply them in a single merge before notifying the UI. Use diff algorithms to compute the minimal changes to apply to the UI, avoiding flickering. Show the expected result immediately and silently correct in the background if the merge differs. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
KEY TAKEAWAYS Write locally first, synchronize in the background. Convergence
Deltas One codebase Known fixes CRDTs guarantee it by mathematical construction. Delta-state CRDTs are the best trade-off for mobile. KMP shares the critical logic across all platforms. Tombstones, pruning and UI glitches have known solutions. The result: a reliable app even when the network is not. -
09 · CONCLUSION Two questions to keep OFFLINE-FIRST 1 Basics
01 Why offline-first? 02 How do you resolve conflicts? 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap -
REFERENCES Thank you @renaudmathieu.com • Shapiro, Preguiça, Baquero, Zawirski, A
comprehensive study of Convergent and Commutative Replicated Data Types (INRIA, 2011) • github.com/ljwagerfield/crdt • github.com/android/nowinandroid OFFLINE-FIRST · DONE 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap