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
We can create a Client using Client(context: Context, rpcHost: String, rpcPort: Int, usePlainText: Boolean, options: Options)
. After the Client has been activated, it is connected to the server and ready to use.
val client = Client(context, "api.yorkie.dev", 443, Options(apiKey = "xxxxx"), false)// Declare your own CoroutineScopescope.launch {client.activateAsync().await}
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.
usePlainText should be set false unless you use it for testing. For more information, please refer to usePlainText.
Subscribing to Client status changes
We can observe various Client status such as status
, streamConnectionStatus
, and peerStatus
.
// Declare your own CoroutineScopescope.launch {client.status.collect {println(it) // "Activated" or "Deactivated"}}scope.launch {client.streamConnectionStatus.collect {println(it) // "Connected" or "Disconnected"}}
By using the value of the streamConnectionStatus
, it is possible to determine whether the Client is connected to the network.
If you want to know about other client events, please refer to Client.Event.
Presence
Presence is a feature that allows you to display information about users who are currently using a collaborative application. Presence is often used in collaborative applications such as document editors, chat apps, and other real-time applications.
val optionA = Client.Options(presence = Presence(mapOf("username" to "alice", "color" to "blue")))val clientA = Client(context, "api.yorkie.dev", 443, optionA)val documentA = Document(Document.Key("doc-1"))// Declare your own CoroutineScopescope.launch {clientA.activateAsync().await()clientA.attachAsync(documentA).await()}
Then, another Client is created and attaches a Document with the same name as before.
val optionB = Client.Options(presence = Presence(mapOf("username" to "bob", "color" to "red")))val clientB = Client(context, "api.yorkie.dev", 443, optionB)val documentB = Document(Document.Key("doc-1"))// Declare your own CoroutineScopescope.launch {clientB.activateAsync().await()clientB.attachAsync(documentB).await()}
When a new peer registers or leaves, the PeersChanged
event is fired, and the other peer's clientID and presence can be obtained from the event.
// Declare your own CoroutineScopescope.launch {clientA.events.filterIsInstance<Client.Event.PeersChanged>().collect { event ->val peers = clientA.peerStatus.value[Document.Key("doc-1")]when (event.result) {is Client.PeersChangedResult.Initialized -> {peers?.let(::displayPeers)}is Client.PeersChangedResult.Watched -> {peers?.forEach { peer -> addPeer(peer) }// peer as follows:// {// clientID: 'xxxxxxxxxxxxxxxxxxxx',// presence: {username: 'bob', color: 'red'}// }}is Client.PeersChangedResult.Unwatched -> {peers?.forEach { peer -> removePeer(peer) }}is Client.PeersChangedResult.PresenceChanged -> {peers?.forEach { peer -> updatePeer(peer) }}}}}
In the code above, clientA
receives a PeersChangedResult.Watched
from clientB
because clientB
attached the Document with the key doc-1
.
Presence can include their names, colors, and other identifying details.
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 peers when the Document is attached to the Client or when the network is restored.
Creating a Document
We can create a Document using Document(key: Key)
. Let's create a Document with a key and attach it to the Client.
val document = Document(Key("doc-1"))// Declare your own CoroutineScopescope.launch {client.attachAsync(document).await()}
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 less than 120 characters.
After attaching the Document to the Client, all changes to the Document are automatically synchronized with remote peers.
Changing Synchronization Setting
To change the synchronization setting for a document, you can use Client.pause(document)
and Client.resume(document)
.
When you pause a document, the synchronization process will no longer occur in realtime, and you will need to manually execute the synchronization to ensure that the changes are propagated to other clients.
To resume the realtime synchronization, you can call Client.resume(document)
.
// Pause real-time syncclient.pause(document)// Resume real-time syncclient.resume(document)
Changing Synchronization Mode
By default, Yorkie synchronizes a document in PushPull
mode, where local changes are pushed to the server, and remote changes are pulled from the server.
If you only want to send your changes and not receive remote changes, you can use PushOnly
mode.
For realtime synchronization, you can use Client.pauseRemoteChanges(document)
and Client.resumeRemoteChanges(document)
.
For manual synchronization, you can pass the desired sync mode to Client.syncAsync(document, syncMode)
.
// Pause remote changes for realtime syncclient.pauseRemoteChanges(document);// Resume remote changes for realtime syncclient.resumeRemoteChanges(document);// Declare your own CoroutineScopescope.launch {// Manual sync in Push-Only modeclient.syncAsync(doc, SyncMode.PushOnly).await();// Manual sync in Push-Pull modeclient.syncAsync(doc, SyncMode.PushPull).await();}
Editing the Document
Document.updateAsync(message, updater)
enables you to modify a Document. The optional message
allows you to add a description to the change. If the Document is attached to the Client, all changes are automatically synchronized with other Clients.
// Declare your own CoroutineScopescope.launch {val message = "update document for test"document.updateAsync(message) { root ->root.setNewObject("obj") // {"obj":{}}root.getAs<JsonObject>("obj")["num"] = 1 // {"obj":{"num":1}}root.getAs<JsonObject>("obj").setNewObject("obj")["str"] = "a" // {"obj":{"num":1,"obj":{"str":"a"}}}root.getAs<JsonObject>("obj").setNewArray("arr").apply { // {"obj":{"num":1,"obj":{"str":"a"},"arr":[1,2]}}put(1)put(2)}}.await()}
Under the hood, root
in the update
function creates a change
, a set of operations, using a JavaScript proxy. Every element has its unique ID, created by the logical clock. This ID is used by Yorkie to track which object is which.
You can get the contents of the Document using document.getRoot()
.
val root = document.getRoot()println(root["obj"]) // {"num":1,"obj":{"str":"a"},"arr":[1,2]}println(root.getAs<JsonObject>("obj")["num"]) // 1println(root.getAs<JsonObject>("obj")["obj"]) // {"str":"a"}println(root.getAs<JsonObject>("obj")["arr"]) // [1,2]
Subscribing to Document events
A Document can be modified by changes generated remotely or locally in Yorkie.
Whenever the Document is modified, change events are triggered and we can subscribe to these events using the document.events
.
The events are triggered with an event object, and the event type indicates the source of the change, which can be one of the following values: LocalChange
, RemoteChange
, or Snapshot
.
When the type of the event is LocalChange
or RemoteChange
, it has ChangeInfo
as value.
For more information about changeInfo for document events, please refer to the ChangeInfo.
// Declare your own CoroutineScopescope.launch {document.events.collect { event ->if (event is Document.Event.LocalChange) {println(event)} else if (event is Document.Event.RemoteChange) {event.changeInfo.operations.forEach { op ->when (op) {is OperationInfo.IncreaseOpInfo -> { // Do something... }else -> { return@collect }}}}}}
Additionally, you can subscribe to changes for a specific path in the Document using document.events(path)
with a path argument, such as $.todos
, where the $
sign indicates the root of the document.
The events are delivered when the target path and its nested values are changed.
// Declare your own CoroutineScopescope.launch {document.events("$.todos").collect { event ->// Events will be delivered when the root.todos or its nested values change.val target = document.getValueByPath("$.todos")// Do something...}
Detaching the Document
If the document is no longer used, it should be detached to increase the efficiency of GC removing CRDT tombstones. For more information about GC, please refer to Garbage Collection.
// Declare your own CoroutineScopescope.launch {client.detachAsync(document).await()}
Custom CRDT types
Custom CRDT types are data types that can be used for special applications such as text editors and counters, unlike general JSON data types such as JsonObject
and JsonArray
. Custom CRDT types can be created in the callback function of document.update
.
JsonText
JsonText
provides supports for collaborative text editing. JsonText
has selection information for sharing the cursor position. In addition, contents in JsonText
can have attributes; for example, characters can be bold, italic, or underlined.
// Declare your own CoroutineScopescope.launch {target.updateAsync { root ->root.setNewText("text") // {"text":[]}root.getAs<JsonText>("text").edit(0, 0, "hello") // {"text":["val":"hello"]}root.getAs<JsonText>("text").edit(0, 1, "H") // {"text":[{"val":"H"},{"val":"ello"}]}root.getAs<JsonText>("text").select(0, 1)root.getAs<JsonText>("text").style(0, 1, mapOf("bold" to "true")) // {"text":[{"attrs":{"bold":"true"},"val":"H"},{"val":"ello"}]}}.await()}
An example of TextEditor: Text Editor example
JsonCounter
JsonCounter
supports integer types changing with addition and subtraction. If an integer data needs to be modified simultaneously, JsonCounter
should be used instead of primitives.
// Declare your own CoroutineScopescope.launch {target.updateAsync { root ->root.setNewCounter("counter", 1) // {"counter":1}root.getAs<JsonCounter>("counter").increase(3) // {"counter":4}root.getAs<JsonCounter>("counter").increase(6) // {"counter":10}root.getAs<JsonCounter>("counter").increase(-3) // {"counter":7}}.await()}
Reference
For details on how to use the Android SDK, please refer to Android SDK Reference.