Android SDK
Through Yorkie Android SDK, you can efficiently build collaborative applications. On the client-side implementation, you can create Documents that are automatically synced with remote peers with minimal effort.
If you want to install the SDK, refer to the Getting Started with Android SDK.
Client
Client is a normal client that can communicate with the server. It has Documents and sends changes of the Document from local to the server to synchronize with other replicas in remote.
Creating a Client
Create a new client instance with the server host and configuration options.
val client = Client(host = "your-yorkie-server-url",options = Client.Options(apiKey = "your-api-key",key = "your-client-key"))
The API key is used to identify the project in Yorkie. You can get the API key of the project you created in the Dashboard.
Activating a Client
Activates the client by registering it with the server. The server assigns a unique client ID that is used to distinguish different clients. After activation, the client is ready to attach documents and communicate with the server.
scope.launch {client.activateAsync().await()}
Client Status
isActive
Returns true if the client is currently activated, false otherwise. Use this to check if the client is ready to use.
if (client.isActive) {// Client is ready to use}
status
Monitor client status changes with a StateFlow. The status can be either Status.Activated (with client ID) or Status.Deactivated.
scope.launch {client.status.collect { status ->when (status) {is Client.Status.Activated -> {println("Client activated with ID: ${status.clientId}")}is Client.Status.Deactivated -> {println("Client is deactivated")}}}}
Attaching Documents
Attaches a document to the client. Once attached, the client will synchronize the document with the server and other clients. You can set initial presence data and choose the synchronization mode.
Parameters:
document: The document to attachinitialPresence: Initial presence data (default: empty map)syncMode: Synchronization mode (default:Realtime)schema: Optional schema key for document validation
val document = Document(Document.Key("my-doc"))scope.launch {client.attachAsync(document,initialPresence = mapOf("name" to "Alice", "color" to "blue"),syncMode = Client.SyncMode.Realtime).await()}
Detaching Documents
Detaches a document from the client. The client will no longer synchronize the document with the server. This is important for garbage collection to clean up CRDT tombstones.
Parameters:
document: The document to detachkeepalive: Iftrue, ensures the detach request completes even if the app terminates (optional, default:false)
scope.launch {client.detachAsync(document).await()}
Synchronizing Documents
Manually pushes local changes to the server and pulls remote changes. Useful when using Manual sync mode or when you need to force synchronization.
Parameters:
document: Specific document to sync. Ifnull, syncs all attached documents (optional)
// Sync a specific documentscope.launch {client.syncAsync(document).await()}// Sync all attached documentsscope.launch {client.syncAsync().await()}
Changing Synchronization Mode
Changes the synchronization mode of an attached document. This allows you to dynamically control how the document synchronizes with the server.
Yorkie offers four SyncModes:
Client.SyncMode.Realtime: Local changes are automatically pushed to the server, and remote changes are pulled from the server.Client.SyncMode.RealtimePushOnly: Only push local changes automatically.Client.SyncMode.RealtimeSyncOff: Changes are not synchronized, but the watch stream remains active.Client.SyncMode.Manual: Synchronization no longer occurs in real-time, and the watch stream is disconnected.
Parameters:
document: The document to updatesyncMode: The new synchronization mode
// Switch to manual sync modeclient.changeSyncMode(document, Client.SyncMode.Manual)// Switch back to realtime syncclient.changeSyncMode(document, Client.SyncMode.Realtime)
Broadcasting Messages
Broadcasts a message to other clients subscribed to the document. This is useful for sending ephemeral messages that don't need to be stored in the document (e.g., notifications, alerts, or temporary states).
Parameters:
document: The document to broadcast totopic: The topic/channel of the messagepayload: The message payload as a string (can be JSON)options: Broadcast options including retry configuration (optional)
scope.launch {client.broadcast(document,topic = "cursor-moved",payload = """{"x": 100, "y": 200}""").await()}
Removing Documents
Permanently removes a document from the server. This operation is irreversible and will delete the document for all clients.
Parameters:
document: The document to remove
scope.launch {client.removeAsync(document).await()}
Deactivating a Client
Deactivates the client and disconnects from the server. All attached documents will be detached automatically.
Parameters:
keepalive: Iftrue, ensures the deactivation request completes even if the app terminates (optional, default:false)
scope.launch {client.deactivateAsync().await()}
Client Options
When creating a client, you can configure various options:
key: Client identifier (default: random UUID)apiKey: API key for the Yorkie projectmetadata: Additional client metadata as key-value pairsfetchAuthToken: Optional function to fetch authentication tokens when neededsyncLoopDuration: Duration between sync loops (default: 50ms)reconnectStreamDelay: Delay before reconnecting after stream disconnect (default: 1000ms)
val client = Client(host = "https://api.yorkie.dev",options = Client.Options(key = "my-client",apiKey = "your-api-key",metadata = mapOf("device" to "mobile"),syncLoopDuration = 100.milliseconds,reconnectStreamDelay = 2.seconds))
Document
Document is a primary data type in Yorkie, which provides a JSON-like updating experience that makes it easy to represent your application's model.
A Document can be updated without being attached to the client, and its changes are automatically propagated to other clients when the Document is attached to the Client or when the network is restored.
Creating a Document
Creates a new document instance with a unique key. The document can be modified locally before being attached to a client.
Parameters:
key: Unique identifier for the documentoptions: Optional configuration (e.g., disable garbage collection)
val document = Document(Document.Key("my-document"))
The document key is used to identify the Document in Yorkie. It is a string that can be freely defined by the user.
However, it is allowed to use only a-z, A-Z, 0-9, -, ., _, ~ and must be between 4-120 characters.
Attaching a Document
Attaches the document to a client. Once attached, the document will be synchronized with the server and other clients. You can set initial presence and choose the sync mode.
scope.launch {client.attachAsync(document,initialPresence = mapOf("name" to "Alice", "color" to "blue"),syncMode = Client.SyncMode.Realtime).await()}
Document Status
The document can be in one of three states:
DocStatus.Attached: Document is attached to a client and synchronizedDocStatus.Detached: Document is not attached to a clientDocStatus.Removed: Document has been permanently removed
when (document.status) {DocStatus.Attached -> println("Document is attached")DocStatus.Detached -> println("Document is detached")DocStatus.Removed -> println("Document is removed")}
Updating a Document
Updates the document structure and/or presence. The updater function provides access to the document root and presence. Changes are automatically synchronized with other clients when attached.
Parameters:
message: Optional description of the changeupdater: Lambda function to modify the document root and/or presence
scope.launch {document.updateAsync("Add user data") { root, _ ->root.setNewObject("user")root.getAs<JsonObject>("user")["name"] = "Alice"root.getAs<JsonObject>("user")["age"] = 30}.await()}
Getting Document Content
getRoot()
Returns a proxy of the document's root object that you can use to read values.
scope.launch {val root = document.getRoot()val user = root.getAs<JsonObject>("user")println("User name: ${user["name"]}")}
getValueByPath()
Gets a value at a specific JSON path. The path must start with $ which represents the document root.
Parameters:
path: JSON path (e.g.,$.user.name,$.todos[0])
scope.launch {val userName = document.getValueByPath("$.user.name")println("User: $userName")}
toJson()
Converts the entire document to a JSON string representation.
val json = document.toJson()println("Document JSON: $json")
Subscribing to Document Events
Subscribe to document events to be notified when changes occur, either locally or from other clients.
Event Types:
Snapshot: Document snapshot received from serverLocalChange: Document changed by current clientRemoteChange: Document changed by another clientPresenceChanged: Presence data changed (see Presence section)SyncStatusChanged: Sync status changed (Synced,SyncFailed)StreamConnectionChanged: Connection status changed (Connected,Disconnected)DocumentStatusChanged: Document status changedBroadcast: Broadcast message receivedAuthError: Authentication error occurred
scope.launch {document.events.collect { event ->when (event) {is Document.Event.Snapshot -> {// Update with data from the Yorkie Document}is Document.Event.LocalChange -> {println("Local change: ${event.changeInfo.message}")}is Document.Event.RemoteChange -> {println("Remote change from: ${event.changeInfo.actorID}")}}}}
events(targetPath)
Subscribe to changes for a specific path in the document. Only events affecting the target path or its nested values will be emitted.
Parameters:
targetPath: JSON path to subscribe to (e.g.,$.todos,$.user.name)
scope.launch {document.events("$.todos").collect { event ->// Events will be delivered when the root.todos or its nested values changeval todos = document.getValueByPath("$.todos")// Handle the change}}
If a client has not synchronized for a prolonged period and then makes a sync request, it might receive a Snapshot
event.
Ensure your application processes these snapshot events correctly to maintain document synchronization.
Changing Synchronization Mode
Changes how the document synchronizes with the server. See the Client Changing Synchronization Mode section for available modes and their descriptions.
// Enable automatic synchronization of both local and remote changesclient.changeSyncMode(document, Client.SyncMode.Realtime)// Only push local changes automaticallyclient.changeSyncMode(document, Client.SyncMode.RealtimePushOnly)// Synchronization turned off, but the watch stream remains activeclient.changeSyncMode(document, Client.SyncMode.RealtimeSyncOff)// Synchronization turned off, and the watch stream is disconnectedclient.changeSyncMode(document, Client.SyncMode.Manual)scope.launch {client.syncAsync(document).await() // Trigger synchronization manually}
Detaching a Document
Detaches the document from the client. The document will no longer be synchronized with the server. This is important for efficient garbage collection to remove CRDT tombstones.
scope.launch {client.detachAsync(document).await()}
Json Types
Yorkie provides several JSON-based data types that can be used to model your application's data. These types are CRDT-based, ensuring automatic conflict resolution in collaborative environments.
JsonObject
JsonObject is a key-value data structure, similar to a JSON object or Kotlin Map. It can contain any JSON-compatible values including other objects, arrays, primitives, and custom CRDT types.
Creating and Using JsonObject:
scope.launch {document.updateAsync { root, _ ->// Create nested objectsroot.setNewObject("user")root.getAs<JsonObject>("user")["name"] = "Alice"root.getAs<JsonObject>("user")["age"] = 30root.getAs<JsonObject>("user").setNewObject("address")val address = root.getAs<JsonObject>("user").getAs<JsonObject>("address")address["city"] = "Seoul"address["country"] = "South Korea"}.await()}
Accessing Values:
scope.launch {val root = document.getRoot()val user = root.getAs<JsonObject>("user")val name = user["name"] // "Alice"val age = user["age"] // 30// Check if key existsval hasEmail = user.has("email") // false// Get value or nullval email = user.getOrNull("email") // null}
Removing Values:
scope.launch {document.updateAsync { root, _ ->val user = root.getAs<JsonObject>("user")user.remove("age")}.await()}
JsonArray
JsonArray is an ordered collection of values, similar to a JSON array or Kotlin List. It supports adding, removing, and accessing elements by index.
Creating and Using JsonArray:
scope.launch {document.updateAsync { root, _ ->// Create an array with primitivesroot.setNewArray("items").apply {put("Apple")put("Banana")put("Cherry")}// Create an array with nested structuresroot.setNewArray("users").apply {putNewObject().apply {set("name", "Alice")set("age", 30)}putNewObject().apply {set("name", "Bob")set("age", 25)}}}.await()}
Accessing and Modifying Elements:
scope.launch {document.updateAsync { root, _ ->val items = root.getAs<JsonArray>("items")// Access by indexval first = items[0] // "Apple"// Update valueitems[1] = "Blueberry"// Remove elementitems.removeAt(2)// Add at specific positionitems.insertAt(0, "Avocado")// Get array sizeval size = items.length}.await()}
Nested Arrays:
scope.launch {document.updateAsync { root, _ ->root.setNewArray("matrix").apply {putNewArray().apply {put(1)put(2)put(3)}putNewArray().apply {put(4)put(5)put(6)}}}.await()}
JsonText
JsonText is a specialized CRDT type for collaborative text editing. It supports text operations like insert, delete, and styling, making it ideal for building rich text editors.
Creating and Editing Text:
scope.launch {document.updateAsync { root, _ ->root.setNewText("content")root.getAs<JsonText>("content").edit(0, 0, "Hello World")}.await()}
Text Operations:
scope.launch {document.updateAsync { root, _ ->val text = root.getAs<JsonText>("content")// Insert text at positiontext.edit(0, 0, "Hello")// Replace text rangetext.edit(0, 5, "Hi") // Replace "Hello" with "Hi"// Delete text rangetext.edit(0, 2, "") // Delete "Hi"// Delete with helper methodtext.delete(0, 5)// Clear all texttext.clear()}.await()}
Styling Text:
scope.launch {document.updateAsync { root, _ ->val text = root.getAs<JsonText>("content")text.edit(0, 0, "Hello World")// Apply styles to text rangetext.style(0, 5, mapOf("bold" to "true"))text.style(6, 11, mapOf("italic" to "true", "color" to "blue"))}.await()}
Edit with Attributes:
scope.launch {document.updateAsync { root, _ ->val text = root.getAs<JsonText>("content")// Insert text with attributestext.edit(0, 0, "Bold text", mapOf("bold" to "true"))}.await()}
Getting Text Properties:
val text = document.getRoot().getAs<JsonText>("content")val length = text.lengthval textValue = text.toString()val values = text.values // List of TextWithAttributes
Text Selection and Presence:
For sharing text cursor positions between users, convert index positions to CRDT positions:
// Convert index range to position range (for presence)document.updateAsync { root, presence ->val text = root.getAs<JsonText>("content")val indexRange = text.edit(from, to, content)val posRange = text.indexRangeToPosRange(indexRange)posRange?.let {presence.put(mapOf("selection" to gson.toJson(posRange)))}}// Convert position range back to index rangedocument.events.filterIsInstance<Document.Event.PresenceChanged.Others>().collect { event ->if (event is Document.Event.PresenceChanged.Others.PresenceChanged) {val (clientID, presence) = event.changedval selectionJson = presence["selection"] as? StringselectionJson?.let {val posRange = gson.fromJson(it, TextPosStructRange::class.java)val indexRange = document.getRoot().getAs<JsonText>("content").posRangeToIndexRange(posRange)// Update editor selection UI}}}
JsonCounter
JsonCounter is a CRDT type that supports atomic increment and decrement operations. It automatically resolves conflicts when multiple clients modify the counter simultaneously.
Creating and Using Counter:
scope.launch {document.updateAsync { root, _ ->// Create counter with initial valueroot.setNewCounter("likes", 0)root.setNewCounter("views", 100)}.await()}
Incrementing and Decrementing:
scope.launch {document.updateAsync { root, _ ->val likes = root.getAs<JsonCounter>("likes")// Increase by positive numberlikes.increase(1)likes.increase(5)// Decrease by negative numberlikes.increase(-2)}.await()}
Getting Counter Value:
val root = document.getRoot()val likes = root.getAs<JsonCounter>("likes")val count = likes.value // CounterValue (Int or Long)
Use Cases:
JsonCounter is ideal for:
- Like counts
- View counts
- Vote tallies
- Collaborative counters
- Any numeric value that multiple users might modify simultaneously
JsonTree
JsonTree is a CRDT-based tree structure designed for representing hierarchical document structures like HTML or XML. It's ideal for building rich text editors with complex formatting (e.g., ProseMirror, Quill).
Creating a Tree:
import dev.yorkie.document.json.TreeBuilder.elementimport dev.yorkie.document.json.TreeBuilder.textscope.launch {document.updateAsync { root, _ ->root.setNewTree("content",element("doc") {element("p") {text { "Hello World" }}element("p") {text { "Second paragraph" }}})}.await()}
Tree with Attributes:
scope.launch {document.updateAsync { root, _ ->root.setNewTree("content",element("doc") {element("p") {text { "Bold text" }attr { "bold" to true }}element("a") {text { "Click here" }attr {"href" to "https://example.com""target" to "_blank"}}})}.await()}
Editing Tree:
scope.launch {document.updateAsync { root, _ ->val tree = root.getAs<JsonTree>("content")// Insert nodes at positiontree.edit(7, 7, // position rangeelement("p") {text { "New paragraph" }})// Delete nodestree.edit(0, 5) // Delete nodes in range}.await()}
Styling Tree Nodes:
scope.launch {document.updateAsync { root, _ ->val tree = root.getAs<JsonTree>("content")// Apply styles to nodes in rangetree.style(0, 5, mapOf("color" to "red", "fontSize" to "16"))}.await()}
Getting Tree Properties:
val root = document.getRoot()val tree = root.getAs<JsonTree>("content")val size = tree.sizeval rootNode = tree.rootTreeNodeval xml = tree.toXml() // Convert tree to XML string
Tree Structure:
JsonTree consists of two types of nodes:
ElementNode: Represents an element with a type, attributes, and childrenTextNode: Represents a text node with a value
Complex Tree Example:
scope.launch {document.updateAsync { root, _ ->root.setNewTree("article",element("doc") {element("h1") {text { "Article Title" }}element("p") {text { "This is " }element("strong") {text { "bold" }}text { " and this is " }element("em") {text { "italic" }}text { "." }}element("ul") {element("li") {text { "First item" }}element("li") {text { "Second item" }}}})}.await()}
Presence
Presence is a feature that allows you to share the temporary state of online users in real-time. Unlike document data, presence information is not stored permanently and is only maintained while users are connected. This makes it ideal for showing user cursors, selections, online status, or any other ephemeral user state.
Setting Initial Presence
When attaching a document, you can set the initial presence for the client using the initialPresence parameter:
Function Signature:
fun attachAsync(document: Document,initialPresence: P = emptyMap(),syncMode: Client.SyncMode = Client.SyncMode.Realtime,schema: String? = null): Deferred<OperationResult>
Parameters:
initialPresence: P- Initial presence data as a map of string key-value pairs (default: empty map)
Example:
scope.launch {client.attachAsync(document,initialPresence = mapOf("name" to "Alice","color" to "#FF5733","status" to "online","avatar" to "https://example.com/avatar.png")).await()}
Updating Presence
You can update presence at any time using document.updateAsync(). The presence parameter in the updater function is a Presence instance.
Function Signature:
fun updateAsync(message: String? = null,updater: suspend (root: JsonObject, presence: Presence) -> Unit): Deferred<OperationResult>
Parameters:
updater: suspend (root: JsonObject, presence: Presence) -> Unit- Lambda withpresenceparameter for updates
Example:
scope.launch {// Update presence only (no document changes)document.updateAsync { _, presence ->presence.put(mapOf("cursor" to "100,200","status" to "typing"))}.await()}
Merge Behavior:
Presence changes are merged, not replaced. Only the specified keys are updated:
// Initial presence: { "name": "Alice", "color": "blue" }scope.launch {document.updateAsync { _, presence ->presence.put(mapOf("status" to "typing"))}.await()}// Result: { "name": "Alice", "color": "blue", "status": "typing" }
Updating Multiple Keys:
scope.launch {document.updateAsync { _, presence ->presence.put(mapOf("cursor" to "200,300","selection" to "5,10","status" to "editing","lastActivity" to System.currentTimeMillis().toString()))}.await()}
Updating Document and Presence Together:
scope.launch {document.updateAsync { root, presence ->// Update documentroot.getAs<JsonText>("content").edit(0, 0, "Hello")// Update presencepresence.put(mapOf("cursor" to "5","status" to "typing"))}.await()}
Presence Events
Presence events notify you when users join, leave, or update their presence. These events are part of Document.Event.PresenceChanged.
MyPresence Events
Events related to the current client:
MyPresence.Initialized: Emitted when the presence state is initialized. Contains all currently online users and their presences.MyPresence.PresenceChanged: Emitted when the current client updates their own presence.
Others Events
Events related to other clients:
Others.Watched: Emitted when another client joins the document.Others.Unwatched: Emitted when another client leaves the document.Others.PresenceChanged: Emitted when another client updates their presence.
Subscribing to Presence Events
Subscribe to presence events before attaching the document to ensure you receive the initial state:
scope.launch {document.events.filterIsInstance<Document.Event.PresenceChanged>().collect { event ->when (event) {is Document.Event.PresenceChanged.MyPresence.Initialized -> {// Initial presence state loaded from serverval onlineUsers = event.initializedprintln("Online users: ${onlineUsers.size}")onlineUsers.forEach { (clientID, presence) ->println("${presence["name"]} is online")}}is Document.Event.PresenceChanged.Others.Watched -> {// Another client joinedval (clientID, presence) = event.changedprintln("${presence["name"]} joined")}is Document.Event.PresenceChanged.Others.Unwatched -> {// Another client leftval (clientID, presence) = event.changedprintln("${presence["name"]} left")}is Document.Event.PresenceChanged.Others.PresenceChanged -> {// Another client updated their presenceval (clientID, presence) = event.changedprintln("${presence["name"]} updated: ${presence["status"]}")}is Document.Event.PresenceChanged.MyPresence.PresenceChanged -> {// Current client's presence updatedval (clientID, presence) = event.changedprintln("My presence updated: $presence")}}}}
Reference
For details on how to use the Android SDK, please refer to Android SDK Reference.