A robust, offline-first Survey Response Sync Engine built for Kotlin Multiplatform (Android & iOS). This library is designed for field agents operating in rural areas with unreliable connectivity, ensuring survey data is captured locally and synced efficiently when a network becomes available.
- Offline-First Storage: Uses Room KMP for reliable local persistence.
- Resilient Sync Logic: Handles partial failures (only retries failed items) and detects network degradation (stops early to save battery).
- Concurrency Protection: Thread-safe sync process using Mutex to prevent duplicate uploads.
- Structured Data Model: Supports complex, nested, and repeating survey sections (e.g., multiple farms per farmer) using a type-safe
SurveyContenthierarchy and Room TypeConverters. - Platform Agnostic: Shared business logic for Android and iOS.
- JDK 17+
- Android Studio / IntelliJ IDEA
- Kotlin 2.2.20+
- KSP (Kotlin Symbol Processing)
git clone https://github.com/KennethMathari/DataSyncEngine.git
cd DataSyncEngineTo use this library in your own project, publish it to your local Maven cache:
# Clean and publish all Android and iOS targets to ~/.m2/repository
./gradlew clean :library:publishToMavenLocal -Dkotlin.compiler.execution.strategy="in-process"Note: GPG signing is disabled for local publishing to simplify the development workflow.
In your consuming project's settings.gradle.kts (or root build.gradle.kts), add mavenLocal():
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
mavenLocal() // Required to find DataSyncEngine
}
}Add the library to your shared module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.pula.sync:library:1.0.0")
}
}
}Define how your app interacts with the network and your specific backend.
import com.pula.sync.engine.SyncApi
import com.pula.sync.engine.NetworkMonitor
import com.pula.sync.data.SurveyResponse
import com.pula.sync.engine.NetworkException
// Check for actual connectivity (e.g., via ConnectivityManager)
class MyNetworkMonitor : NetworkMonitor {
override fun isConnected(): Boolean = true
}
// Handle the actual network call to your server
class MyBackendApi : SyncApi {
override suspend fun upload(response: SurveyResponse): Result<String> {
return try {
// httpClient.post(...)
Result.success("server_id_123")
} catch (e: Exception) {
Result.failure(NetworkException("Network timeout"))
}
}
}import com.pula.sync.data.getDatabaseBuilder
import com.pula.sync.engine.SyncManager
import com.pula.sync.data.SurveyResponse
import com.pula.sync.data.SurveyContent
import com.pula.sync.data.SurveyStatus
import kotlinx.datetime.Clock
class SurveyRepository(context: Any?) {
// 1. Initialize Database (pass Context on Android, null on iOS)
private val database = getDatabaseBuilder(context).build()
// 2. Initialize Manager
private val syncManager = SyncManager(
dao = database.surveyDao(),
api = MyBackendApi(),
networkMonitor = MyNetworkMonitor()
)
// 3. Capture survey offline
suspend fun saveSurvey(id: String, content: SurveyContent) {
val response = SurveyResponse(
id = id,
content = content,
attachments = emptyList(),
status = SurveyStatus.PENDING,
createdAt = Clock.System.now().toEpochMilliseconds()
)
database.surveyDao().insert(response)
}
// 4. Trigger Sync (Background or Manual)
suspend fun startSync() {
val completed = syncManager.sync()
if (completed) println("Sync finished!")
}
}For detailed architectural decisions, including media handling, GPS validation, and remote troubleshooting strategies, see ARCHITECTURE.md.
Run the cross-platform test suite:
./gradlew :library:allTestsLicensed under the Apache License, Version 2.0.