Skip to content
potluckdb

Android

macOS · Linux · Windows · Containers · iOS and iPadOS · Android · ChromeOS · Browser · Node (WebAssembly)

potluckdb ships two Android targets, arm64-v8a and x86_64, in one AAR. It contains the desktop jar’s dev.potluckdb classes and both ABIs’ runtime and JNI libraries. Kotlin uses the Java classes.

C++ links the ABI’s C SDK archive. Other Android frameworks reach the same runtime through their framework packages.

Android 13 (API level 33) or newer. The AAR declares minSdkVersion 33. A module with a lower minSdk fails manifest merging.

LanguagePackageWhat it selects herePages
JavaScript@potluckdb/potluckdb-react-native, through React Native · @potluckdb/potluckdb-capacitor, through Capacitor and Ionicthe C SDK archive of the target, in the framework’s packageInstall · Quickstart
TypeScript@potluckdb/potluckdb-react-native, through React Native · @potluckdb/potluckdb-capacitor, through Capacitor and Ionicthe C SDK archive of the target, in the framework’s packageInstall · Quickstart
Javadev.potluckdb:potluckdbABIs arm64-v8a, x86_64Install · Quickstart
Kotlindev.potluckdb:potluckdbABIs arm64-v8a, x86_64Install · Quickstart
Dartpotluckdb, through Flutterthe requested target’s C SDK archive, verified by the build hookInstall · Quickstart
Flutterpotluckdb on pub.devthe requested target’s C SDK archive, verified by the build hookInstall · Quickstart
C# (.NET)potluckdb, through .NET MAUIthe C SDK archive of the target, in the framework’s packageInstall · Quickstart
C++the C SDK archivethe C SDK archive of the targetInstall · Quickstart
Cthe C SDK archivethe C SDK archive of the targetInstall · Quickstart

Not shipped for this platform: Python, Swift, Objective-C, Go, Rust.

TargetABIC SDK archive
android-arm64-v8aarm64-v8apotluckdb-c-sdk-0.1.0-android-arm64-v8a.zip
android-x86_64x86_64potluckdb-c-sdk-0.1.0-android-x86_64.zip

The package is in potluckdb’s Maven repository.

  1. Add the repository in settings.gradle.kts.
  2. In the module’s build.gradle.kts, set minSdk to 33 and add the dependency.

The boxes show only the lines to add. If settings.gradle.kts already has a dependencyResolutionManagement block, add only the maven line to its repositories.

settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://downloads.potluckdb.com/maven")
}
}
// build.gradle.kts of the module
android {
defaultConfig {
minSdk = 33
}
}
dependencies {
implementation("dev.potluckdb:potluckdb:0.1.0")
}

Remote storage. The AAR contains the storage library for both ABIs. The JVM storage service constructors work unchanged. Supply session credentials from the application’s backend.

Before the first potluckdb call, set POTLUCKDB_CONFIG_DIR under the application’s files directory, because Android sets no home directory. The licensing page explains this requirement.

Add these imports to the file of your launcher activity, and add this override to the activity class. If the activity already overrides onCreate, add only the Os.setenv line at the start of that method, before super.onCreate. This is an activity excerpt, not a complete program.

import android.os.Bundle
import android.system.Os
override fun onCreate(savedInstanceState: Bundle?) {
Os.setenv("POTLUCKDB_CONFIG_DIR", "$filesDir/configuration", false)
super.onCreate(savedInstanceState)
}

The self-contained program represents a laptop and phone with two databases sharing one remote. They modify the same row before either synchronizes, then reach the same state. The remote is a temporary directory that the program removes on exit.

This is the JVM quickstart in Kotlin. The release test suite compiles it with kotlinc against the desktop jar and runs it on the desktop JVM. In an Android module, the same file compiles against the AAR. Replace the scratch directory with context.filesDir and the directory remote with a bucket.

Save it as Quickstart.kt. It prints Use query for this statement. and then quickstart: ok.

// Two devices, one remote, one conflict, and its resolution.
import dev.potluckdb.Bindings
import dev.potluckdb.Database
import dev.potluckdb.OpenOptions
import dev.potluckdb.PotluckDB
import dev.potluckdb.PotluckDBException
import dev.potluckdb.Remote
import dev.potluckdb.SyncPolicy
import dev.potluckdb.SyncResult
import java.nio.file.Files
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
/** Every sync attempt reports an outcome, UpToDate included; wait for the requested one. */
fun <T : SyncResult> waitFor(events: LinkedBlockingQueue<SyncResult>, wanted: Class<T>): T {
while (true) {
val outcome = events.poll(10, TimeUnit.SECONDS) ?: error("no sync outcome within ten seconds")
if (outcome is SyncResult.Failed) throw outcome.error()
if (wanted.isInstance(outcome)) return wanted.cast(outcome)
}
}
/** One Pushed is one automatic cycle's work, not proof that everything written so far is published. */
fun waitUntilPublished(device: Database, events: LinkedBlockingQueue<SyncResult>) {
while (device.status().pendingWrites() > 0) {
waitFor(events, SyncResult.Pushed::class.java)
}
}
fun expect(condition: Boolean, message: String) = check(condition) { message }
fun main() {
val scratch = Files.createTempDirectory("potluckdb-")
try {
Remote.fromDirectory(scratch.resolve("remote").toString()).use { remote -> // a bucket replaces this in production; see the installation page
// The laptop synchronizes on its own: automatic is the default policy.
// onSync delivers every outcome to the program; this program queues them.
val laptopEvents = LinkedBlockingQueue<SyncResult>()
val laptopOptions = OpenOptions.builder().onSync { outcome -> laptopEvents.add(outcome) }.build()
val phoneOptions = OpenOptions.builder().syncPolicy(SyncPolicy.manual()).build()
PotluckDB.open(scratch.resolve("laptop").toString(), remote, laptopOptions).use { laptop ->
PotluckDB.open(scratch.resolve("phone").toString(), remote, phoneOptions).use { phone ->
laptop.transaction { setup -> // one transaction: both statements travel in one push
setup.execute("CREATE TABLE note(id INTEGER PRIMARY KEY, body TEXT NOT NULL)")
setup.execute("INSERT INTO note(body) VALUES(?)", Bindings.positional("buy milk"))
null
}
waitUntilPublished(laptop, laptopEvents)
// The phone synchronizes only when instructed: a manual policy, and sync()
// calls that return the outcome as a value. It starts from the revision the
// laptop published.
expect(phone.sync() is SyncResult.Pulled, "the phone pulls the laptop's published revision")
val rows = phone.query("SELECT id, body FROM note ORDER BY id")
expect(rows.columns() == listOf("id", "body"), "columns are reported with the rows")
expect(rows[0]["body"] == "buy milk" && rows[0][0] == 1L, "a row is read by column name or position")
// Both devices modify the same row before either has seen the other's write.
laptop.execute("UPDATE note SET body = ? WHERE id = ?", Bindings.positional("buy oat milk", 1))
phone.execute("UPDATE note SET body = ? WHERE id = ?", Bindings.positional("buy milk and eggs", 1))
waitUntilPublished(laptop, laptopEvents)
// The phone's push finds a remote head it has not seen:
// a Conflict, returned rather than thrown.
expect(phone.sync() is SyncResult.Conflict, "the phone's push is refused with a conflict")
val resolution = phone.resolveConflict() // adopt the laptop's revision, replay the phone's statement on it
expect(resolution.quarantined().isEmpty(), "no statement was quarantined")
expect(phone.sync() is SyncResult.Pushed, "the phone's push succeeds after resolution")
// The laptop pulls the resolved revision on its own.
// Both devices hold the same row.
waitFor(laptopEvents, SyncResult.Pulled::class.java)
for (db in listOf(laptop, phone)) {
val current = db.query("SELECT body FROM note WHERE id = ?", Bindings.positional(1))
expect(current[0]["body"] == "buy milk and eggs", "both devices hold the same row")
}
// query reads, execute writes. The wrong call is refused before execution.
try {
laptop.execute("SELECT count(*) FROM note")
expect(false, "execute refuses a read")
} catch (error: PotluckDBException) {
expect(error.code() == "read_in_execute", "the refusal names the code")
println(error.action()) // Use query for this statement.
}
}
}
}
} finally {
scratch.toFile().deleteRecursively()
}
println("quickstart: ok")
}
  1. Download potluckdb-jvm-0.1.0.jar into the directory that contains Quickstart.kt.
  2. Compare its SHA-256 digest with the bare hexadecimal digest in the digest file.
  3. Compile and run with these commands.
Terminal window
kotlinc -cp potluckdb-jvm-0.1.0.jar Quickstart.kt -include-runtime -d quickstart.jar
java --enable-native-access=ALL-UNNAMED -cp quickstart.jar:potluckdb-jvm-0.1.0.jar QuickstartKt

Analytical reads load a DuckDB library built for the ABI. The analytics engine page lists both libraries and their digests. Place the library for each ABI under jniLibs/<abi> in the module, and name it in the open options.

Each release candidate builds every target in the table and passes every required check of the release matrix before promotion. The packages that this page names are the packages that the release tested.

Each release builds the AAR for both ABIs and compiles and runs this Kotlin program on the desktop JVM. The release does not run this program on an Android emulator. The frameworks below run their own programs there.

Each release also runs framework quickstarts on this platform: Flutter, React Native, Capacitor and Ionic, .NET MAUI. Each framework page describes its verification.