Kotlin SDK
Build native Android and JVM applications that integrate with Staqr Platform and Commerce APIs.
Security Notice
Never embed API keys directly in Android applications.
Instead, use one of these patterns:
- OAuth2/OIDC user authentication
- Backend proxy for sensitive API calls
- Token-based authentication with refresh
See Mobile Security for detailed guidance.
Installation
Gradle (Kotlin DSL)
dependencies {
implementation("com.staqr:staqr-api-client-kotlin:1.0.0")
}
Gradle (Groovy)
implementation 'com.staqr:staqr-api-client-kotlin:1.0.0'
Maven
<dependency>
<groupId>com.staqr</groupId>
<artifactId>staqr-api-client-kotlin</artifactId>
<version>1.0.0</version>
</dependency>
Quick Start
import com.staqr.client.StaqrApiClient
import com.staqr.client.api.FlowsApi
// Configuration - token from secure storage
val token = SecureTokenStorage(context).getToken()
val client = StaqrApiClient(
basePath = "https://api.staqr.com",
accessToken = token
)
// Using coroutines
suspend fun loadFlows() {
try {
val flows = client.flowsApi.listFlows(limit = 50)
println("Found ${flows.size} flows")
flows.forEach { flow ->
println(" - ${flow.name} (${flow.id})")
}
} catch (e: Exception) {
println("Error: ${e.message}")
}
}
Available Packages
| Package | API | Min SDK |
|---|---|---|
staqr-api-client-kotlin | Staqr Platform | API 21 (Android 5.0) |
staqr-commerce-v0-kotlin | Commerce v0 (Legacy) | API 21 |
staqr-commerce-v1-kotlin | Commerce v1 | API 21 |
staqr-commerce-v2-kotlin | Commerce v2 | API 21 |
Authentication
EncryptedSharedPreferences Storage
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
class SecureTokenStorage(context: Context) {
private val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
private val prefs = EncryptedSharedPreferences.create(
context,
"staqr_secure_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun saveToken(token: String) {
prefs.edit().putString("access_token", token).apply()
}
fun getToken(): String? {
return prefs.getString("access_token", null)
}
fun deleteToken() {
prefs.edit().remove("access_token").apply()
}
fun saveRefreshToken(token: String) {
prefs.edit().putString("refresh_token", token).apply()
}
fun getRefreshToken(): String? {
return prefs.getString("refresh_token", null)
}
}
OAuth2 Integration with AppAuth
import net.openid.appauth.*
class AuthService(private val context: Context) {
private val serviceConfig = AuthorizationServiceConfiguration(
Uri.parse("https://auth.staqr.com/authorize"),
Uri.parse("https://auth.staqr.com/token")
)
fun createAuthRequest(): AuthorizationRequest {
return AuthorizationRequest.Builder(
serviceConfig,
"your-client-id",
ResponseTypeValues.CODE,
Uri.parse("your-app://callback")
)
.setScope("openid profile email")
.build()
}
suspend fun exchangeCodeForToken(
response: AuthorizationResponse
): TokenResponse = suspendCancellableCoroutine { continuation ->
val authService = AuthorizationService(context)
authService.performTokenRequest(
response.createTokenExchangeRequest()
) { tokenResponse, exception ->
when {
tokenResponse != null -> continuation.resume(tokenResponse) {}
exception != null -> continuation.resumeWithException(exception)
else -> continuation.resumeWithException(IllegalStateException("Unknown error"))
}
}
}
}
Android Architecture Components
ViewModel with Coroutines
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
sealed class FlowsUiState {
object Loading : FlowsUiState()
data class Success(val flows: List<Flow>) : FlowsUiState()
data class Error(val message: String) : FlowsUiState()
}
class FlowsViewModel(
private val staqrClient: StaqrApiClient
) : ViewModel() {
private val _uiState = MutableStateFlow<FlowsUiState>(FlowsUiState.Loading)
val uiState: StateFlow<FlowsUiState> = _uiState.asStateFlow()
init {
loadFlows()
}
fun loadFlows() {
viewModelScope.launch {
_uiState.value = FlowsUiState.Loading
try {
val flows = staqrClient.flowsApi.listFlows(limit = 100)
_uiState.value = FlowsUiState.Success(flows)
} catch (e: Exception) {
_uiState.value = FlowsUiState.Error(
e.message ?: "Failed to load flows"
)
}
}
}
fun refreshFlows() {
loadFlows()
}
}
Hilt Dependency Injection
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object StaqrModule {
@Provides
@Singleton
fun provideSecureTokenStorage(
@ApplicationContext context: Context
): SecureTokenStorage {
return SecureTokenStorage(context)
}
@Provides
@Singleton
fun provideStaqrClient(
tokenStorage: SecureTokenStorage
): StaqrApiClient {
return StaqrApiClient(
basePath = "https://api.staqr.com",
accessToken = tokenStorage.getToken()
)
}
@Provides
@Singleton
fun provideFlowsApi(client: StaqrApiClient): FlowsApi {
return client.flowsApi
}
}
Jetpack Compose Integration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.hilt.navigation.compose.hiltViewModel
@Composable
fun FlowsScreen(
viewModel: FlowsViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = { Text("Flows") },
actions = {
IconButton(onClick = { viewModel.refreshFlows() }) {
Icon(Icons.Default.Refresh, "Refresh")
}
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when (val state = uiState) {
is FlowsUiState.Loading -> {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
is FlowsUiState.Success -> {
LazyColumn {
items(state.flows) { flow ->
FlowListItem(flow = flow)
}
}
}
is FlowsUiState.Error -> {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.message,
color = MaterialTheme.colorScheme.error
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.refreshFlows() }) {
Text("Retry")
}
}
}
}
}
}
}
@Composable
fun FlowListItem(flow: Flow) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = flow.name,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = flow.id,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Error Handling
import retrofit2.HttpException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val exception: Throwable) : ApiResult<Nothing>()
}
suspend fun <T> safeApiCall(apiCall: suspend () -> T): ApiResult<T> {
return try {
ApiResult.Success(apiCall())
} catch (e: Exception) {
ApiResult.Error(e)
}
}
fun handleApiError(error: Throwable): String {
return when (error) {
is HttpException -> {
when (error.code()) {
401 -> "Session expired. Please sign in again."
403 -> "You don't have permission to perform this action."
404 -> "Resource not found."
in 500..599 -> "Server error. Please try again later."
else -> "Request failed. Please try again."
}
}
is UnknownHostException -> "No internet connection."
is SocketTimeoutException -> "Request timed out."
else -> error.message ?: "An unexpected error occurred."
}
}
// Usage
viewModelScope.launch {
when (val result = safeApiCall { staqrClient.flowsApi.listFlows() }) {
is ApiResult.Success -> {
_uiState.value = FlowsUiState.Success(result.data)
}
is ApiResult.Error -> {
val message = handleApiError(result.exception)
_uiState.value = FlowsUiState.Error(message)
}
}
}
Network Security Configuration
Create res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.staqr.com</domain>
<domain includeSubdomains="true">auth.staqr.com</domain>
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</domain-config>
</network-security-config>
Reference in AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
... >
ProGuard Rules
Add to proguard-rules.pro:
# Staqr SDK
-keep class com.staqr.client.** { *; }
-keepclassmembers class com.staqr.client.** { *; }
# kotlinx.serialization
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
Requirements
- Kotlin 1.6+
- Android API 21+ (for Android apps)
- JDK 11+ (for JVM apps)
Dependencies
The SDK uses:
OkHttpfor HTTP networkingkotlinx.serializationfor JSON parsingkotlinx.coroutinesfor async operations
Next Steps
- Mobile Security Guide - Critical security patterns
- Authentication Guide - Token management
- API Reference - Full endpoint documentation