Android Client

Official SDK v0.5.0 API 26+ (Android 8.0)

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
hostStringlocalhostServer hostname or IP
portInt6745Server port
timeoutLong30000Connection timeout (ms)
enableCompressionBooleantrueEnable 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)UnitCreate collection (DOCUMENT or EDGE)
deleteCollection(name)UnitDelete collection
collectionStats(name)DocumentGet 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)DocumentInsert document, returns with key
save(collection, key, document)DocumentInsert with custom key
get(collection, key)DocumentGet document by key
delete(collection, key)UnitDelete 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.
Native Performance

UniFFI-powered Rust bindings deliver native speed for all operations.

Local SQLite

Encrypted SQLite storage for offline data persistence with SQLCipher.

Auto-Sync

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_WINSDocument with most recent timestamp wins
FIRST_WRITE_WINSFirst write wins, subsequent rejected
SERVER_WINSAlways prefer server version
CLIENT_WINSAlways prefer local version
CUSTOMProvide 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: