Android SDK
Fetch, verify, cache, and serve Ed25519-signed string bundles in Android apps, Jetpack Compose ready.
The AirStrings Android SDK fetches remotely managed localized strings, verifies every bundle's Ed25519 signature before exposing a single string, and caches verified bundles for offline use.
Requirements: Android API 26+ (Android 8.0), Kotlin 2.0+.
Install
The SDK is distributed via JitPack. Maven Central distribution is planned.
Add the JitPack repository
In your root settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}Add the dependency
In your module's build.gradle.kts:
dependencies {
implementation("com.github.symbionix-sl:airstrings-sdk-android:v1.1.1")
}Setup
Create a configuration and an AirStrings instance. Only organizationId, projectId, environmentId, and publicKeys are required.
import com.airstrings.sdk.AirStrings
import com.airstrings.sdk.AirStringsConfiguration
val config = AirStringsConfiguration(
organizationId = "org_a1b2c3d4e5f6",
projectId = "proj_a1b2c3d4e5f6",
environmentId = "env_a1b2c3d4e5f6",
publicKeys = listOf("BASE64_ED25519_PUBLIC_KEY"),
)
val airStrings = AirStrings.create(applicationContext, config)| Field | Type | Required | Description |
|---|---|---|---|
organizationId | String | yes | Your AirStrings organization ID. |
projectId | String | yes | Project ID. |
environmentId | String | yes | Environment ID (e.g. production, staging). |
publicKeys | List<String> | yes | One or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation. |
locale | AirStringsLocale | no | AirStringsLocale.System (device locale, default) or AirStringsLocale.Fixed("en-US"). |
apiBaseURL | String | no | API base URL. Defaults to https://api.airstrings.com. |
seedEnabled | Boolean | no | Set false to disable bundled fallback seeding. Defaults to true. |
seedDirectory | String | no | Directory probed for the bundled fallback seed. Defaults to "airstrings/bundles". |
AirStrings.create always uses the application context: pass any Context and the SDK calls applicationContext internally. Find your IDs and public key in the dashboard under Project Settings > SDK Configuration, or in the setup snippet shown after you publish a bundle.
Usage
Jetpack Compose
Provide the instance at the root of your composition, then collect the strings StateFlow:
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.airstrings.sdk.compose.LocalAirStrings
@Composable
fun MyApp(airStrings: AirStrings) {
CompositionLocalProvider(LocalAirStrings provides airStrings) {
WelcomeScreen()
}
}
@Composable
fun WelcomeScreen() {
val airStrings = LocalAirStrings.current
val strings by airStrings.strings.collectAsStateWithLifecycle()
Text(strings["onboarding.welcome_title"] ?: "onboarding.welcome_title")
}operator fun get reads strings.value synchronously and does NOT trigger recomposition. In Compose, always collect the strings StateFlow via collectAsStateWithLifecycle() so views update when bundles change.
Outside Compose
val title = airStrings["onboarding.welcome_title"]Returns the raw value, or the key name itself as a fallback when no bundle has loaded.
Locale
Fix the locale at config time:
val config = AirStringsConfiguration(
organizationId = "org_a1b2c3d4e5f6",
projectId = "proj_a1b2c3d4e5f6",
environmentId = "env_a1b2c3d4e5f6",
publicKeys = listOf("BASE64_ED25519_PUBLIC_KEY"),
locale = AirStringsLocale.Fixed("fr-FR"),
)Or switch at runtime with a suspend call:
airStrings.setLocale("es")Previous strings stay visible until the new locale loads, with no flash of raw keys.
String variants
String variants are A/B experiments: a variant is selected deterministically from a stable assignment id, entirely on-device, with no server round-trip. Set an assignment id once and forward exposures to your analytics:
// Stable per-user (or per-device) id — same id always maps to the same variant
airStrings.setAssignmentId(userId)
// Forward exposures to your analytics. Fires once per (key, experiment, variant, assignment),
// asynchronously on the main thread.
airStrings.onExposure = { event ->
analytics.track(
"experiment_exposure",
mapOf(
"experiment" to event.experimentId,
"variant" to event.variant,
"key" to event.key,
),
)
}
// Reads now return the assigned variant's value
val cta = airStrings["checkout.cta"]
// Clear the assignment to return to base values
airStrings.setAssignmentId(null)Reads are unchanged: airStrings["checkout.cta"] returns the assigned variant's value, and onExposure provides an ExposureEvent (a public data class).
Experiment content is Ed25519-verified via a separate experiments_signature and soft-fails to base values if verification fails: variants are never served unverified. Passing null to setAssignmentId() clears the assignment and returns to base values. Available since Android SDK 1.1.0.
See String Variants for how selection, bucketing, and exposure work, and managing experiments from the CLI to create one.
ICU formatting
Every string has a format: "text" (plain) or "icu" (ICU MessageFormat). Use format(key, args) to format ICU strings via the platform android.icu.text.MessageFormat:
airStrings.format("items.count", mapOf("count" to 5))text strings are returned as-is (arguments ignored). Pattern syntax: ICU by example.
Caching & offline
- On create, the SDK loads the cached bundle from disk (if any) and fetches the latest from the CDN.
- Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is a hard error and the bundle is rejected.
- Verified bundles are cached per-locale under
context.cacheDir/airstrings/and re-verified on every load (defense in depth). - Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
- The SDK auto-refreshes when the app enters the foreground, using
ETag/If-None-Match(304 Not Modified) to avoid re-downloading unchanged bundles. - With no cache and no network,
isReadystaysfalseand string keys are returned as a fallback. All failure paths are silent: the SDK keeps serving cached strings.
AirStrings implements Closeable. For application-scoped usage cleanup is automatic; for scoped usage, call airStrings.close() to cancel coroutines and remove the lifecycle observer.
Multiple publicKeys support rotation; see Key rotation.
To ship published, signed bundles inside your app for offline-safe first launches, pull them with airstrings bundles pull and commit the generated seed directory. See Offline & fallback for how seeding, caching, and key-name fallback interact.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
Strings show as raw keys (e.g. onboarding.welcome_title) | No bundle has loaded yet (no cache + first launch offline), or no bundle is published for this locale | Publish a bundle for the locale; confirm organizationId, projectId, and environmentId are correct. |
Text never recomposes after an update | Reading via airStrings["key"] instead of collecting the StateFlow | Use collectAsStateWithLifecycle() on airStrings.strings in Compose. |
| Strings never update after publishing | Wrong environmentId, or the bundle was published to a different environment | Verify the environment ID matches the one you published from. |
| Bundle silently rejected | publicKeys does not contain the key that signed the bundle | Copy the environment's public key from Project Settings > SDK Configuration. After rotation, include both old and new keys. |