Android SDK
Kotlin · Android API 26+ · Gradle
The MAIG Android SDK provides a native Kotlin interface for generating text and streaming responses from any AI model configured in your project. Requests are authenticated with your project API key and routed through MAIG's gateway — your provider credentials never leave the server.
Requirements
- Kotlin 1.9 or later
- Java 17 or later
- Android API 26 or later (or any JVM target)
- A MAIG project API key — see the Dashboard Setup guide
Installation
Gradle
Add the dependency to your module's build.gradle.kts:
dependencies {
implementation("com.maig:sdk:0.1.0")
}
Local build
Clone the repository and publish to your local Maven repository:
git clone https://github.com/your-org/maig-android-sdk.git
cd maig-android-sdk
./gradlew :sdk:publishToMavenLocal
Then add mavenLocal() to your project's repository list:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
}
}
Initialization
Create one shared AIGatewayClient instance — typically in a ViewModel or a dependency injection module:
import com.maig.sdk.AIGatewayClient
val client = AIGatewayClient(apiKey = "maig_YOUR_PROJECT_KEY")
To target a local or staging gateway, pass a custom baseUrl:
val client = AIGatewayClient(
apiKey = "maig_YOUR_PROJECT_KEY",
baseUrl = "https://staging.maig.dev"
)
BuildConfig using a local local.properties entry or a secrets manager.
Generating text
Use generateText to make a single round-trip request and receive the complete response as a String. It is a suspend function and must be called from a coroutine:
import com.maig.sdk.AIGatewayClient
import com.maig.sdk.GenerateOptions
val client = AIGatewayClient(apiKey = "maig_YOUR_KEY")
// suspend function — call from a coroutine or viewModelScope
val response = client.generateText(
prompt = "Summarise the following article in three bullet points: ...",
options = GenerateOptions(model = "gpt-4o", maxTokens = 512),
)
println(response)
Named routes
Pass a route name as the model field to target a specific route you configured in the dashboard. This decouples your app from concrete model names — you can change the underlying model in the dashboard without shipping an app update:
// Target the route named "chat" in your project
val options = GenerateOptions(model = "chat")
val response = client.generateText(
prompt = "Summarise the following article in three bullet points: ...",
options = options,
)
You can also pass a literal model name (e.g. "gpt-4o") to bypass route selection entirely.
GenerateOptions
| Field | Type | Default | Description |
|---|---|---|---|
model | String? | "auto" | Route name or literal model identifier; null uses the project default |
userId | String? | null | Stable end-user ID for rate limiting; appears in dashboard logs |
maxTokens | Int? | null | Token budget for the response |
Retry behaviour
generateText retries up to 2 times with exponential backoff (1 s, 2 s) on transient server and network errors. Auth failures are never retried.
Streaming text
Use streamText to receive tokens as they are generated. The method returns a Kotlin Flow<String> that emits tokens as they arrive:
client.streamText(prompt = "Write a haiku about coroutines")
.collect { token -> print(token) }
Streaming uses Server-Sent Events (SSE) parsed incrementally over an OkHttp response body. streamText does not retry — let the caller decide whether to restart the flow.
ViewModel example
A typical pattern for an Android chat screen:
class ChatViewModel : ViewModel() {
private val client = AIGatewayClient(apiKey = BuildConfig.MAIG_API_KEY)
val response = MutableStateFlow("")
fun ask(prompt: String) {
viewModelScope.launch {
try {
client.streamText(prompt).collect { token ->
response.value += token
}
} catch (e: AIGatewayError.AuthFailure) {
// handle invalid key
} catch (e: AIGatewayError.ServerError) {
// handle server error (e.statusCode, e.body)
} catch (e: AIGatewayError.NetworkError) {
// handle connectivity issues
}
}
}
}
Error handling
Both generateText and streamText throw subtypes of the AIGatewayError sealed class:
try {
val response = client.generateText("Hello")
display(response)
} catch (e: AIGatewayError.AuthFailure) {
// HTTP 401 — bad or expired API key; not retried
showError("Authentication failed. Check your API key.")
} catch (e: AIGatewayError.ServerError) {
// Gateway or upstream provider returned a non-2xx response
println("HTTP ${e.statusCode}: ${e.body}")
} catch (e: AIGatewayError.NetworkError) {
// Device is offline or connection was lost
println("Network problem: ${e.cause?.message}")
}
Error cases
AuthFailure— the project API key is missing, malformed, or revoked (HTTP 401); never retriedServerError(statusCode, body)— the gateway responded with a non-2xx HTTP status; includes the status code and an optional response bodyNetworkError(cause)— transport-level failure; the associated value is the underlying exception
Prompt Management
The SDK includes PromptStore — a lightweight class that syncs server-managed prompts to a local cache on device. Update prompts in the dashboard and your app picks up the change on its next sync() call, no app release required.
val store = PromptStore(apiKey = "maig_YOUR_PROJECT_KEY", context = applicationContext)
// At app launch — fetches only changed prompts
store.sync()
// At inference time — synchronous in-memory read, no network call
val storedMessages = store.getPrompt("support-bot-system") ?: emptyList()
val messages = storedMessages + Message(role = "user", content = "My order hasn't arrived.")
val result = client.generateText(messages = messages)
See the Prompt Management guide for full details on creating prompts in the dashboard, handling the first-launch case, and the complete API reference.
Semantic versioning
Prompts are versioned as major.minor (e.g. v1.2). A minor bump (v1.0 → v1.1) is a safe edit — no new variables. A major bump (v1.x → v2.0) means a new {{VARIABLE}} was added and your app code needs to supply it.
Version pinning
Pin a prompt to a major version so your app only ever receives minor updates within that major. A major bump is never delivered until you explicitly move the pin — protecting you from broken variable substitution at runtime.
Option A — JSON config file (default location)
Add maig-prompts.json to your Android project's src/main/assets/ folder. The SDK looks for this file automatically — no extra code needed:
{
"pinned": {
"support-bot": 1,
"onboarding-flow": 2
}
}
// SDK loads maig-prompts.json from assets automatically
val store = PromptStore(apiKey = "maig_YOUR_PROJECT_KEY", context = applicationContext)
// Custom filename: override the default
val store = PromptStore(
apiKey = "maig_YOUR_PROJECT_KEY",
context = applicationContext,
configFile = "my-config.json",
)
// Pass null to disable config file loading entirely
val store = PromptStore(
apiKey = "maig_YOUR_PROJECT_KEY",
context = applicationContext,
configFile = null,
)
Option B — Runtime API
val store = PromptStore(apiKey = "maig_YOUR_PROJECT_KEY", context = applicationContext)
// Pin before calling sync() — runtime pins override the JSON file
store.pin("support-bot", majorVersion = 1)
store.pin("onboarding-flow", majorVersion = 2)
store.sync()
When you are ready to adopt a new major version, update the pin number and handle the new variable in your app code:
// Ready for v2 — bump the pin and supply the new variable
store.pin("support-bot", majorVersion = 2)
store.sync()
val result = store.getPrompt("support-bot", mapOf(
"PRODUCT" to "Acme Store",
"TIER" to userTier, // new variable added in v2
))
if (result != null) {
val messages = result.messages + Message(role = "user", content = userInput)
}
Next steps
- Configure fallback routing in the dashboard to improve resilience when a provider is unavailable
- View per-request logs, token counts, and latency in the dashboard's Logs tab
- Manage prompts server-side with Prompt Management
- Questions? Email support@maig.dev