SDKs

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)
FieldTypeRequiredDescription
organizationIdStringyesYour AirStrings organization ID.
projectIdStringyesProject ID.
environmentIdStringyesEnvironment ID (e.g. production, staging).
publicKeysList<String>yesOne or more base64-encoded Ed25519 public keys. Multiple keys supported for rotation.
localeAirStringsLocalenoAirStringsLocale.System (device locale, default) or AirStringsLocale.Fixed("en-US").
apiBaseURLStringnoAPI base URL. Defaults to https://api.airstrings.com.
seedEnabledBooleannoSet false to disable bundled fallback seeding. Defaults to true.
seedDirectoryStringnoDirectory 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

  1. On create, the SDK loads the cached bundle from disk (if any) and fetches the latest from the CDN.
  2. Every bundle is Ed25519-signed. Verification is mandatory: an invalid signature is a hard error and the bundle is rejected.
  3. Verified bundles are cached per-locale under context.cacheDir/airstrings/ and re-verified on every load (defense in depth).
  4. Anti-downgrade protection: a newer revision is never replaced by an older one for the same locale.
  5. The SDK auto-refreshes when the app enters the foreground, using ETag / If-None-Match (304 Not Modified) to avoid re-downloading unchanged bundles.
  6. With no cache and no network, isReady stays false and 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

SymptomLikely causeFix
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 localePublish a bundle for the locale; confirm organizationId, projectId, and environmentId are correct.
Text never recomposes after an updateReading via airStrings["key"] instead of collecting the StateFlowUse collectAsStateWithLifecycle() on airStrings.strings in Compose.
Strings never update after publishingWrong environmentId, or the bundle was published to a different environmentVerify the environment ID matches the one you published from.
Bundle silently rejectedpublicKeys does not contain the key that signed the bundleCopy the environment's public key from Project Settings > SDK Configuration. After rotation, include both old and new keys.

On this page