Kotlin / Android
Installation
Section titled “Installation”Gradle (Kotlin DSL)
Section titled “Gradle (Kotlin DSL)”dependencies { implementation("fit.trainstar:synchro:0.1.0")}Gradle (Groovy)
Section titled “Gradle (Groovy)”dependencies { implementation 'fit.trainstar:synchro:0.1.0'}Android: minSdk 24, compileSdk 34
Dependencies: OkHttp 4.12+, Kotlinx Serialization 1.6+, Kotlinx Coroutines 1.8+
Configuration
Section titled “Configuration”val config = SynchroConfig( dbPath = "synchro.db", serverURL = "https://api.example.com", authProvider = { getToken() }, clientID = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID), appVersion = "1.0.0")val client = SynchroClient(config, context)SynchroConfig Parameters
Section titled “SynchroConfig Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
dbPath | String | Required | SQLite database file name or path |
serverURL | String | Required | Sync server base URL |
authProvider | suspend () -> String | Required | Returns a JWT token for authentication |
clientID | String | Required | Unique device identifier |
platform | String | "android" | Platform name sent during registration |
appVersion | String | Required | Semantic version of the app |
syncInterval | Double | 30.0 | Seconds between sync cycles |
pushDebounce | Double | 0.5 | Seconds after a write before triggering push |
maxRetryAttempts | Int | 5 | Maximum retry count before entering error state |
pullPageSize | Int | 100 | Rows per pull page (validated 1—1000) |
pushBatchSize | Int | 100 | Pending changes per push batch (validated 1—1000) |
snapshotPageSize | Int | 100 | Rows per snapshot page (validated 1—1000) |
Core Usage
Section titled “Core Usage”Queries
Section titled “Queries”// Fetch multiple rowsval rows = client.query( "SELECT * FROM tasks WHERE user_id = ?", params = arrayOf(userId))
// Fetch a single rowval task = client.queryOne( "SELECT * FROM tasks WHERE id = ?", params = arrayOf(id))Row is a type alias for Map<String, Any?>. Column values are returned as their SQLite-native types: Long for integers, Double for floats, String for text, ByteArray for blobs, and null for NULL.
Writes
Section titled “Writes”val result = client.execute( "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", params = arrayOf(UUID.randomUUID().toString(), "Review proposal", userId))// result.rowsAffected == 1Batch Execution
Section titled “Batch Execution”val affected = client.executeBatch(listOf( SQLStatement( sql = "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", params = arrayOf(UUID.randomUUID().toString(), "Write report", userId) ), SQLStatement( sql = "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", params = arrayOf(UUID.randomUUID().toString(), "Update docs", userId) ),))// affected == 2Transactions
Section titled “Transactions”// Write transaction: multiple operations atomicallyclient.writeTransaction { db -> db.execSQL( "INSERT INTO tasks (id, title, user_id) VALUES (?, ?, ?)", arrayOf(UUID.randomUUID().toString(), "Review proposal", userId) ) db.execSQL( "INSERT INTO comments (id, task_id, body, status) VALUES (?, ?, ?, ?)", arrayOf(UUID.randomUUID().toString(), taskId, "Looks good", "open") )}
// Read transaction: consistent snapshotval count = client.readTransaction { db -> db.rawQuery("SELECT COUNT(*) FROM tasks", null).use { cursor -> if (cursor.moveToFirst()) cursor.getInt(0) else 0 }}Observation
Section titled “Observation”Change Notification
Section titled “Change Notification”val cancellable = client.onChange(listOf("tasks")) { println("tasks table changed")}
// Later: stop observingcancellable.cancel()Reactive Query
Section titled “Reactive Query”val cancellable = client.watch( "SELECT * FROM tasks ORDER BY created_at DESC", tables = listOf("tasks")) { rows -> this.tasks = rows}The callback fires immediately with the current result set, then again whenever the observed tables change. The SDK uses a SQL-parsing heuristic to detect which tables are affected by each write.
Sync Control
Section titled “Sync Control”// Start sync: register, fetch schema, begin sync loopclient.start()
// Trigger an immediate sync cycleclient.syncNow()
// Stop the sync loopclient.stop()
// Close the database and HTTP connectionsclient.close()Status and Events
Section titled “Status and Events”Sync Status
Section titled “Sync Status”val cancellable = client.onStatusChange { status -> when (status) { is SyncStatus.Idle -> println("Idle") is SyncStatus.Syncing -> println("Syncing...") is SyncStatus.Error -> { val retryAt = status.retryAt if (retryAt != null) { println("Error, retrying at $retryAt") } else { println("Error, no retry scheduled") } } is SyncStatus.Stopped -> println("Stopped") }}SyncStatus is a sealed class:
sealed class SyncStatus { data object Idle : SyncStatus() data object Syncing : SyncStatus() data class Error(val retryAt: java.time.Instant?) : SyncStatus() data object Stopped : SyncStatus()}Conflict Events
Section titled “Conflict Events”val cancellable = client.onConflict { event -> println("Conflict on ${event.table} record ${event.recordID}") println("Client data: ${event.clientData}") println("Server data: ${event.serverData}")}Snapshot Required
Section titled “Snapshot Required”val cancellable = client.onSnapshotRequired { // Prompt the user or decide programmatically // Return true to proceed with snapshot, false to abort promptUser("Full resync needed. Continue?")}Error Handling
Section titled “Error Handling”All errors are represented by the SynchroError sealed class:
sealed class SynchroError(message: String, cause: Throwable? = null) : Exception(message, cause) {
class NotConnected : SynchroError(...) class SchemaNotLoaded : SynchroError(...) class TableNotSynced(val table: String) : SynchroError(...) class UpgradeRequired( val currentVersion: String, val minimumVersion: String ) : SynchroError(...) class SchemaMismatch( val serverVersion: Long, val serverHash: String ) : SynchroError(...) class SnapshotRequired : SynchroError(...) class PushRejected(val results: List<PushResult>) : SynchroError(...) class NetworkError(val underlying: Throwable) : SynchroError(...) class ServerError( val status: Int, val serverMessage: String ) : SynchroError(...) class DatabaseError(val underlying: Throwable) : SynchroError(...) class InvalidResponse(val details: String) : SynchroError(...) class AlreadyStarted : SynchroError(...) class NotStarted : SynchroError(...)}| Class | When It Occurs |
|---|---|
NotConnected | Operation attempted before start() completes registration |
SchemaNotLoaded | Schema has not been fetched from server yet |
TableNotSynced | Write attempted on a table not in the server schema |
UpgradeRequired | Server rejected the client’s app version (HTTP 426) |
SchemaMismatch | Client schema hash does not match server (HTTP 409) |
SnapshotRequired | Server indicates a full snapshot is needed |
PushRejected | One or more push records were rejected by the server |
NetworkError | Network connectivity failure |
ServerError | Server returned a non-200 HTTP status |
DatabaseError | SQLite operation failed |
InvalidResponse | Server response could not be decoded |
AlreadyStarted | start() called when sync is already running |
NotStarted | syncNow() called before start() |
Errors follow standard Kotlin exception handling:
try { client.start()} catch (e: SynchroError.UpgradeRequired) { showUpgradeDialog(e.minimumVersion)} catch (e: SynchroError.NetworkError) { showOfflineBanner()} catch (e: SynchroError) { log.error("Sync error", e)}