Skip to content

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()
    }
}