Android Client
Getting Started
Installation
Gradle (build.gradle.kts)
dependencies {
implementation("com.solidb:solidb-android:0.5.0")
// Optional: For Kotlin coroutines support
implementation("com.solidb:solidb-android-coroutines:0.5.0")
}
Maven (pom.xml)
<dependency>
<groupId>com.solidb</groupId>
<artifactId>solidb-android</artifactId>
<version>0.5.0</version>
</dependency>
Android Manifest Permissions
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- For background sync (optional) --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
ProGuard Rules
# SoliDB Android SDK
-keep class com.solidb.android.** { *; }
-keep class com.solidb.sdk.** { *; }
-keepclassmembers class * {
@com.solidb.sdk.annotations.* <fields>;
}
# Kotlinx.serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** { *; }
-keepclassmembers class * extends kotlinx.serialization.Serializer { *; }
Requirements: minSdk 26 (Android 8.0), Kotlin 1.8+. The SDK uses UniFFI for Rust bindings and supports both Java and Kotlin with full coroutines integration.
Quick Start
import com.solidb.android.SoliDBClient
import com.solidb.android.SoliDBConfig
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var client: SoliDBClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launch {
setupDatabase()
}
}
private suspend fun setupDatabase() {
try {
// Create client with configuration
val config = SoliDBConfig(
host = "localhost",
port = 6745,
database = "mydb",
username = "admin",
password = "password"
)
client = SoliDBClient.create(config)
// Basic CRUD operations
val doc = client.save(
collection = "users",
document = mapOf(
"name" to "Alice",
"age" to 30,
"email" to "[email protected]"
)
)
Log.d("SoliDB", "Created document with key: ${doc.key}")
// Retrieve document
val user = client.get(
collection = "users",
key = doc.key
)
Log.d("SoliDB", "Retrieved: ${user["name"]}")
// Query with SDBQL
val results = client.query(
query = "FOR u IN users FILTER u.age > @min RETURN u",
bindVars = mapOf("min" to 25)
)
Log.d("SoliDB", "Found ${results.size} users")
} catch (e: Exception) {
Log.e("SoliDB", "Error: ${e.message}")
}
}
}
Connection Management
// Configure connection
val config = SoliDBConfig(
host = "localhost",
port = 6745,
database = "mydb",
username = "admin",
password = "password",
timeout = 30_000L, // 30 seconds
enableCompression = true
)
// Create client
val client = SoliDBClient.create(config)
// Check connection health
val isHealthy = client.ping()
Log.d("SoliDB", "Connection healthy: $isHealthy")
// Disconnect when done (in onDestroy)
override fun onDestroy() {
super.onDestroy()
lifecycleScope.launch {
client.disconnect()
}
}
| Property | Type | Default | Description |
|---|---|---|---|
host | String | localhost | Server hostname or IP |
port | Int | 6745 | Server port |
timeout | Long | 30000 | Connection timeout (ms) |
enableCompression | Boolean | true | Enable MessagePack compression |
Authentication
// Authentication is handled during client initialization
val config = SoliDBConfig(
host = "localhost",
port = 6745,
database = "mydb", // Target database
username = "admin", // Username
password = "password" // Password
)
val client = SoliDBClient.create(config)
// The session remains authenticated until disconnected
// Token is automatically refreshed when needed
Core Operations
Collection Operations
// List collections in a database
val collections = client.listCollections()
// => ["users", "orders", "products"]
// Create a document collection
client.createCollection(name = "products")
// Create an edge collection (for graphs)
client.createCollection(
name = "relationships",
type = CollectionType.EDGE
)
// Delete a collection
client.deleteCollection(name = "old_collection")
// Get collection statistics
val stats = client.collectionStats(name = "users")
Log.d("SoliDB", "Document count: ${stats["document_count"]}")
| Method | Returns | Description |
|---|---|---|
listCollections() | List<String> | List collections in database |
createCollection(name:type) | Unit | Create collection (DOCUMENT or EDGE) |
deleteCollection(name) | Unit | Delete collection |
collectionStats(name) | Document | Get collection statistics |
Document Operations (CRUD)
// SAVE - Create or update a document
val doc = client.save(
collection = "users",
document = mapOf(
"name" to "Alice",
"email" to "[email protected]",
"age" to 30,
"tags" to listOf("developer", "android")
)
)
Log.d("SoliDB", "Key: ${doc.key}") // Auto-generated key
// SAVE with custom key
val docWithKey = client.save(
collection = "users",
key = "user-123",
document = mapOf(
"name" to "Bob",
"email" to "[email protected]"
)
)
// GET - Retrieve a document by key
val user = client.get(
collection = "users",
key = "user-123"
)
Log.d("SoliDB", "Name: ${user["name"]}")
// DELETE - Remove a document
client.delete(
collection = "users",
key = "user-123"
)
// LIST - Paginated document listing
val (docs, total) = client.list(
collection = "users",
limit = 50,
offset = 0
)
Log.d("SoliDB", "Showing ${docs.size} of $total documents")
| Method | Returns | Description |
|---|---|---|
save(collection, document) | Document | Insert document, returns with key |
save(collection, key, document) | Document | Insert with custom key |
get(collection, key) | Document | Get document by key |
delete(collection, key) | Unit | Delete document |
list(collection, limit, offset) | Pair<List<Document>, Int> | List documents with pagination |
SDBQL Queries
// Simple query
val users = client.query("FOR u IN users RETURN u")
// Query with bind variables (recommended for security)
val results = client.query(
query = """
FOR u IN users
FILTER u.age >= @min_age AND u.status == @status
SORT u.created_at DESC
LIMIT @limit
RETURN { name: u.name, email: u.email }
""",
bindVars = mapOf(
"min_age" to 18,
"status" to "active",
"limit" to 100
)
)
// Aggregation query
val stats = client.query(
query = """
FOR u IN users
COLLECT status = u.status WITH COUNT INTO count
RETURN { status, count }
"""
)
// Join query
val orders = client.query(
query = """
FOR o IN orders
FOR u IN users FILTER u._key == o.user_id
RETURN { order: o, user: u.name }
"""
)
Live Sync & Subscriptions
// Subscribe to live query updates
val subscription = client.sync.subscribe(
query = "FOR u IN users FILTER u.status == 'online' RETURN u"
) { result ->
when (result) {
is SyncResult.Success -> {
val docs = result.documents
Log.d("SoliDB", "Received ${docs.size} updates")
// Update your UI
}
is SyncResult.Error -> {
Log.e("SoliDB", "Sync error: ${result.error}")
}
}
}
// Subscribe to collection changes
val collectionSub = client.sync.subscribe(
collection = "users",
filter = mapOf("status" to "active")
) { change ->
when (change.type) {
ChangeType.INSERT -> {
Log.d("SoliDB", "New document: ${change.document.key}")
}
ChangeType.UPDATE -> {
Log.d("SoliDB", "Updated: ${change.document.key}")
}
ChangeType.DELETE -> {
Log.d("SoliDB", "Deleted: ${change.key}")
}
}
}
// Cancel subscriptions when done
subscription.cancel()
collectionSub.cancel()
// Manual sync (pull latest changes)
client.sync.pull()
// Push local changes immediately
client.sync.push()
| Method | Description |
|---|---|
subscribe(query, handler) | Subscribe to query results |
subscribe(collection, filter, handler) | Subscribe to collection changes |
pull() | Pull latest changes from server |
push() | Push local changes to server |
Offline-First Sync
Offline-First Architecture
SoliDB Android SDK provides automatic offline-first capabilities. All operations work seamlessly offline and sync when connectivity is restored.
// Enable offline-first mode in configuration
val config = SoliDBConfig(
host = "localhost",
port = 6745,
database = "mydb",
username = "admin",
password = "password",
offlineMode = OfflineMode.ENABLED, // Enable offline support
enableLocalCache = true, // Cache documents locally
syncInterval = 30_000L // Auto-sync every 30 seconds
)
// All operations work offline
// Changes are automatically queued and synced
val doc = client.save(
collection = "notes",
document = mapOf(
"title" to "Important Note",
"content" to "..."
)
)
// Works even without network! Automatically syncs later.
UniFFI-powered Rust bindings deliver native speed for all operations.
Encrypted SQLite storage for offline data persistence with SQLCipher.
Automatic background sync with WorkManager for reliability.
Conflict Resolution
When the same document is modified offline on multiple devices, SoliDB provides configurable conflict resolution strategies.
// Configure conflict resolution strategy
val config = SoliDBConfig(
// ... other options
conflictResolution = ConflictResolution.LAST_WRITE_WINS, // Default
// Other options:
// ConflictResolution.FIRST_WRITE_WINS
// ConflictResolution.SERVER_WINS
// ConflictResolution.CLIENT_WINS
// ConflictResolution.CUSTOM(resolver)
)
// Custom conflict resolver
val customConfig = SoliDBConfig(
conflictResolution = ConflictResolution.CUSTOM { serverDoc, localDoc ->
// Implement your own logic
val localModified = localDoc["modified_at"] as? Date
val serverModified = serverDoc["modified_at"] as? Date
if (localModified != null && serverModified != null) {
if (localModified > serverModified) localDoc else serverDoc
} else {
localDoc // Default to local
}
}
)
| Strategy | Description |
|---|---|
LAST_WRITE_WINS | Document with most recent timestamp wins |
FIRST_WRITE_WINS | First write wins, subsequent rejected |
SERVER_WINS | Always prefer server version |
CLIENT_WINS | Always prefer local version |
CUSTOM | Provide your own resolution logic |
Delta Sync
Delta sync only transmits changed fields, dramatically reducing bandwidth and improving sync speed.
// Delta sync is enabled by default
val config = SoliDBConfig(
// ... other options
enableDeltaSync = true, // Only sync changed fields
deltaSyncThreshold = 1024 // Use delta for docs > 1KB
)
// Manual delta sync control
// Force full sync when needed
client.sync.pullFull()
// Sync specific collections with delta
client.sync.pull(
collections = listOf("users", "settings"),
mode = SyncMode.DELTA
)
// Get sync statistics
val syncStats = client.sync.statistics
Log.d("SoliDB", "Bytes saved by delta sync: ${syncStats.bytesSaved}")
Log.d("SoliDB", "Documents synced: ${syncStats.documentsSynced}")
Sync Configuration
val config = SoliDBConfig(
host = "localhost",
port = 6745,
database = "mydb",
username = "admin",
password = "password",
// Offline settings
offlineMode = OfflineMode.ENABLED,
enableLocalCache = true,
localCacheSize = 100 * 1024 * 1024L, // 100 MB cache limit
// Sync settings
syncInterval = 30_000L, // Auto-sync every 30s
syncOnConnectivityChange = true, // Sync when going online
retryAttempts = 5, // Retry failed syncs
retryDelay = 2_000L, // Initial retry delay (ms)
// Conflict & delta
conflictResolution = ConflictResolution.LAST_WRITE_WINS,
enableDeltaSync = true,
// Network
enableCompression = true,
timeout = 30_000L
)
Examples
Complete Todo App
A fully functional Todo app demonstrating offline-first capabilities, real-time sync, and CRUD operations using Jetpack Compose.
Model & ViewModel
import com.solidb.android.SoliDBClient
import com.solidb.android.SoliDBConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
// MARK: - Model
data class Todo(
val id: String,
val title: String,
val completed: Boolean = false,
val createdAt: Long = System.currentTimeMillis()
) {
companion object {
fun fromDocument(doc: Document): Todo {
return Todo(
id = doc.key,
title = doc["title"] as? String ?: "",
completed = doc["completed"] as? Boolean ?: false,
createdAt = doc["created_at"] as? Long ?: System.currentTimeMillis()
)
}
}
fun toDocument(): Map<String, Any> = mapOf(
"title" to title,
"completed" to completed,
"created_at" to createdAt
)
}
// MARK: - ViewModel
class TodoViewModel(
private val client: SoliDBClient
) : ViewModel() {
private val _todos = MutableStateFlow<List<Todo>>(emptyList())
val todos: StateFlow<List<Todo>> = _todos
private val _isSyncing = MutableStateFlow(false)
val isSyncing: StateFlow<Boolean> = _isSyncing
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage: StateFlow<String?> = _errorMessage
private var subscription: SyncSubscription? = null
init {
loadTodos()
subscribeToChanges()
}
private fun loadTodos() {
viewModelScope.launch {
try {
val results = client.query(
"FOR t IN todos SORT t.created_at DESC RETURN t"
)
_todos.value = results.map { Todo.fromDocument(it) }
} catch (e: Exception) {
_errorMessage.value = "Failed to load: ${e.message}"
}
}
}
private fun subscribeToChanges() {
viewModelScope.launch {
try {
subscription = client.sync.subscribe(
collection = "todos"
) { change ->
// Refresh on any change
loadTodos()
}
} catch (e: Exception) {
Log.e("TodoViewModel", "Subscription error: ${e.message}")
}
}
}
fun addTodo(title: String) {
viewModelScope.launch {
try {
client.save(
collection = "todos",
document = mapOf(
"title" to title,
"completed" to false,
"created_at" to System.currentTimeMillis()
)
)
// Real-time update will come through subscription
} catch (e: Exception) {
_errorMessage.value = "Failed to add: ${e.message}"
}
}
}
fun toggleTodo(todo: Todo) {
viewModelScope.launch {
try {
client.save(
collection = "todos",
key = todo.id,
document = todo.copy(completed = !todo.completed).toDocument()
)
} catch (e: Exception) {
_errorMessage.value = "Failed to update: ${e.message}"
}
}
}
fun deleteTodo(todo: Todo) {
viewModelScope.launch {
try {
client.delete(collection = "todos", key = todo.id)
} catch (e: Exception) {
_errorMessage.value = "Failed to delete: ${e.message}"
}
}
}
fun syncNow() {
viewModelScope.launch {
_isSyncing.value = true
try {
client.sync.push()
client.sync.pull()
loadTodos()
} catch (e: Exception) {
_errorMessage.value = "Sync failed: ${e.message}"
} finally {
_isSyncing.value = false
}
}
}
override fun onCleared() {
super.onCleared()
subscription?.cancel()
}
}
Jetpack Compose UI
@Composable
fun TodoScreen(viewModel: TodoViewModel = viewModel()) {
val todos by viewModel.todos.collectAsState()
val isSyncing by viewModel.isSyncing.collectAsState()
val errorMessage by viewModel.errorMessage.collectAsState()
var newTodoTitle by remember { mutableStateOf("") }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Todos") },
actions = {
IconButton(onClick = { viewModel.syncNow() }) {
if (isSyncing) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
strokeWidth = 2.dp
)
} else {
Icon(Icons.Default.Refresh, contentDescription = "Sync")
}
}
}
)
},
floatingActionButton = {
// Add todo dialog or inline input
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
) {
// Add new todo
OutlinedTextField(
value = newTodoTitle,
onValueChange = { newTodoTitle = it },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
placeholder = { Text("Add new todo...") },
trailingIcon = {
if (newTodoTitle.isNotBlank()) {
IconButton(onClick = {
viewModel.addTodo(newTodoTitle)
newTodoTitle = ""
}) {
Icon(Icons.Default.Add, contentDescription = "Add")
}
}
}
)
// Todo list
LazyColumn {
items(todos, key = { it.id }) { todo ->
TodoItem(
todo = todo,
onToggle = { viewModel.toggleTodo(todo) },
onDelete = { viewModel.deleteTodo(todo) }
)
}
}
}
// Error snackbar
errorMessage?.let { message ->
LaunchedEffect(message) {
// Show snackbar
}
}
}
}
@Composable
fun TodoItem(
todo: Todo,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.completed,
onCheckedChange = { onToggle() }
)
Text(
text = todo.title,
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyLarge,
textDecoration = if (todo.completed) {
TextDecoration.LineThrough
} else {
TextDecoration.None
}
)
IconButton(onClick = onDelete) {
Icon(Icons.Default.Delete, contentDescription = "Delete")
}
}
}
}
Coroutines & Flow Integration
// Convert sync subscriptions to Flow
fun SoliDBClient.todosFlow(): Flow<List<Todo>> = callbackFlow {
val subscription = sync.subscribe(
query = "FOR t IN todos SORT t.created_at DESC RETURN t"
) { result ->
when (result) {
is SyncResult.Success -> {
trySend(result.documents.map { Todo.fromDocument(it) })
}
is SyncResult.Error -> {
close(result.error)
}
}
}
awaitClose { subscription.cancel() }
}
// Use with Compose
@Composable
fun TodoList(client: SoliDBClient) {
val todos by client.todosFlow().collectAsState(initial = emptyList())
LazyColumn {
items(todos) { todo ->
TodoItem(todo = todo)
}
}
}
// Background sync with WorkManager
class SyncWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val client = SoliDBClient.create(config)
return try {
client.sync.push()
client.sync.pull()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
// Schedule periodic sync
val syncWorkRequest = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"solidb_sync",
ExistingPeriodicWorkPolicy.KEEP,
syncWorkRequest
)
Error Handling
try {
val client = SoliDBClient.create(config)
} catch (e: SoliDBException.ConnectionFailed) {
Log.e("SoliDB", "Connection failed: ${e.message}")
} catch (e: SoliDBException.AuthenticationFailed) {
Log.e("SoliDB", "Auth failed: ${e.message}")
} catch (e: SoliDBException.DocumentNotFound) {
Log.e("SoliDB", "Document not found: ${e.key}")
} catch (e: SoliDBException.NetworkError) {
Log.e("SoliDB", "Network error: ${e.message}")
// Will auto-retry in offline mode
} catch (e: SoliDBException.SyncConflict) {
Log.e("SoliDB", "Sync conflict: ${e.message}")
// Resolve manually if needed
} catch (e: SoliDBException) {
Log.e("SoliDB", "SoliDB error: ${e.message}")
} catch (e: Exception) {
Log.e("SoliDB", "Unknown error: ${e.message}")
}
// With Result type wrapper
suspend fun safeGet(client: SoliDBClient, key: String): Result<Document> {
return try {
Result.success(client.get("todos", key))
} catch (e: SoliDBException.DocumentNotFound) {
Result.failure(e)
} catch (e: Exception) {
Result.failure(e)
}
}
Next Steps
Explore more features of the SoliDB Android SDK: