Android Jetpack Room Integration
This guide demonstrates how to integrate AirDB into an existing Android application using Android Jetpack Room.
Architecture Overview
+-------------------------------------------------------------+
| Android Application UI (Jetpack Compose / ViewModel) |
+------------------------------+------------------------------+
| Queries / Updates
+------------------------------v------------------------------+
| AppDatabase (Room) |
| - Domain Tables: survivor_entity, pos_orders |
| - Changelog Table: airdb_changelog (append-only sync feed) |
+------------------------------+------------------------------+
|
+------------------------------v------------------------------+
| RoomAirDbAdapter (Implements AirDbAdapter) |
| - getChangesSince(hlc, limit) -> Reads airdb_changelog |
| - applyChanges(changes) -> Applies LWW merge to Room tables |
| - getLatestHlc() -> SELECT MAX(hlc_timestamp) |
+------------------------------+------------------------------+
|
+------------------------------v------------------------------+
| AirDbEngine (Causal Ordering, BLE Transport, Cryptography) |
+-------------------------------------------------------------+
1. Define the Changelog Entity
Add an append-only changelog entity alongside your domain entities in your Room database:
package com.example.app.data.local
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "airdb_changelog",
indices = [
Index(value = ["hlcTimestamp"], unique = false),
Index(value = ["collection", "recordId"], unique = false)
]
)
data class ChangelogEntity(
@PrimaryKey(autoGenerate = true)
val localId: Long = 0,
val collection: String,
val recordId: String,
val mutationType: String, // INSERT, UPDATE, DELETE
val payload: ByteArray,
val hlcTimestamp: String,
val authorPublicKey: ByteArray? = null,
val signature: ByteArray? = null,
val createdAt: Long = System.currentTimeMillis()
)
2. Define the DAO
package com.example.app.data.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface ChangelogDao {
@Query("SELECT * FROM airdb_changelog WHERE hlcTimestamp > :sinceHlc ORDER BY hlcTimestamp ASC LIMIT :limit")
suspend fun getChangesSince(sinceHlc: String, limit: Int): List<ChangelogEntity>
@Query("SELECT * FROM airdb_changelog ORDER BY hlcTimestamp ASC LIMIT :limit")
suspend fun getAllChanges(limit: Int): List<ChangelogEntity>
@Query("SELECT MAX(hlcTimestamp) FROM airdb_changelog")
suspend fun getLatestHlc(): String?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(changes: List<ChangelogEntity>)
}
3. Implement the AirDbAdapter
package com.example.app.data.local
import io.airdb.adapter.AirDbAdapter
import io.airdb.adapter.ChangeRecord
import io.airdb.adapter.HlcComparator
import io.airdb.adapter.MutationType
class RoomAirDbAdapter(
private val database: AppDatabase,
private val changelogDao: ChangelogDao
) : AirDbAdapter {
override suspend fun getChangesSince(sinceHlcTimestamp: String?, limit: Int): List<ChangeRecord> {
val entities = if (sinceHlcTimestamp.isNullOrEmpty()) {
changelogDao.getAllChanges(limit)
} else {
changelogDao.getChangesSince(sinceHlcTimestamp, limit)
}
return entities.map { it.toChangeRecord() }
}
override suspend fun applyChanges(changes: List<ChangeRecord>) {
database.runInTransaction {
// Apply each change to your domain tables using Last-Write-Wins
for (change in changes) {
// Insert into changelog for downstream gossip propagation
// and update corresponding domain entity
}
}
}
override suspend fun getLatestHlc(): String? {
return changelogDao.getLatestHlc()
}
}