跪拜 Guibai
← Back to the summary

Syncox Brings Fire-and-Forget Offline Sync to Kotlin Multiplatform

Hello everyone. When writing client-side business requirements in daily work, you must deeply detest the following scenario:

Whenever we need to perform an operation like form submission, liking, or canceling an order, the ViewModel is usually filled with boilerplate code like this to handle network requests:

fun submitData() {
    _uiState.update { it.copy(isLoading = true) }
    viewModelScope.launch {
        try {
            val response = apiService.submit(...)
            _uiState.update { it.copy(isLoading = false, isSuccess = true) }
        } catch (e: Exception) {
            // Weak network, disconnection, timeout all end up here...
            _uiState.update { it.copy(isLoading = false) }
            showToast("Network connection failed, please try again later!")
        }
    }
}

This paradigm of "blocking the foreground and waiting for a network response" has several fatal pain points:

  1. Terrible experience: In weak network conditions, users can only stare at a loading spinner, eventually receiving an error.
  2. State explosion: You need to maintain a cumbersome state machine of Loading, Success, and Error.
  3. Easy data loss: If the request hasn't succeeded and the App is killed by the system, the user's operation is completely lost.

Why do WeChat messages, or high-frequency interactions in some major apps, feel so smooth even when there's no network on the subway? Because they use a backend-like Outbox Pattern at the bottom layer.

The core idea is: Don't make the UI wait for the network! Data is directly persisted to the local database, and the UI instantly returns success. The rest is left to a background daemon process to retry.

To elegantly implement this pattern in the team's KMP and Android projects without polluting the existing network code, I hand-crafted an extremely lightweight offline synchronization engine open-source library — Syncox.

What is Syncox?

Syncox is a client-side offline synchronization engine specifically built for Kotlin Multiplatform (KMP) and Android.

Its core design philosophy is "Zero Intrusion" and "Fire-and-Forget". You don't need to refactor your existing Ktor or Retrofit interfaces; just add one annotation, and your App gains enterprise-level network disconnection resilience.

See How Simple Integration Is

No need to learn complex architectural concepts. In just three steps, you can experience the satisfaction of eliminating try-catch:

Step 1: Wrap the data (Action)

Define a data class that implements the SyncoxAction interface. This step tells the engine: "What data do I want to store" and "Which interface should this data be sent to".

@Serializable
data class CreatePostRequest(val title: String, val body: String, val userId: Int)

class CreatePostAction(request: CreatePostRequest) : SyncoxAction {
    override val actionType: String = "CREATE_POST" // Routing key
    override val payloadJson: String = Json.encodeToString(request) // Automatically converts to JSON for persistence
}

Step 2: Add an annotation to the original network interface

This is the most satisfying part of Syncox. Thanks to KSP compile-time code generation technology, you only need to annotate the actual request function with @OfflineSync. You never need to manually call this function in business code again; KSP automatically generates a reflection-free, safe routing table, allowing the underlying background process to find it on its own!

// This method is automatically scheduled by the underlying engine and will automatically retry on error
@OfflineSync(action = "CREATE_POST")
suspend fun executeCreatePost(payloadJson: String): NetworkResult {
    return try {
        val request = Json.decodeFromString<CreatePostRequest>(payloadJson)
        val response = httpClient.post("posts", request)
        
        if (response.status.isSuccess()) {
            NetworkResult.Success // Success: Engine automatically erases the record from SQLite
        } else {
            NetworkResult.Failure(isFatal = false) // Temporary server error: Engine automatically triggers backoff retry
        }
    } catch (e: Exception) {
        NetworkResult.Failure(isFatal = false, error = e) // Disconnected: Engine enters silent waiting
    }
}

Step 3: One-click send in the ViewModel

Now, your ViewModel is clean with just one line of code. No Loading, no try-catch.

// Instantly persists to the local database, taking < 1ms. The UI thread won't lag at all!
Syncox.enqueue(CreatePostAction(request))

// If the UI layer wants to show "syncing in background", just bind this Flow to Compose
val pendingCountFlow: Flow<Int> = Syncox.observePendingCount()

Summary and Open Source Address

If you are also tired of handling endless network exceptions in the ViewModel, or if you want your App to behave more "premium" and smoother under weak network conditions, I highly recommend trying this Outbox Pattern.

The project is now fully open source, supporting KMP cross-platform projects (Android, iOS, Desktop) and pure Android projects:

👉 GitHub Address: https://github.com/SamiuZhong/Syncox

The project has just started and is still under active iteration. Experts are welcome to review, raise Issues, or submit PRs. If you find the idea inspiring, a Star ⭐ would be greatly appreciated!