Skip to main content

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:

  1. OAuth2/OIDC user authentication
  2. Backend proxy for sensitive API calls
  3. 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

PackageAPIMin SDK
staqr-api-client-kotlinStaqr PlatformAPI 21 (Android 5.0)
staqr-commerce-v0-kotlinCommerce v0 (Legacy)API 21
staqr-commerce-v1-kotlinCommerce v1API 21
staqr-commerce-v2-kotlinCommerce v2API 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:

  • OkHttp for HTTP networking
  • kotlinx.serialization for JSON parsing
  • kotlinx.coroutines for async operations

Next Steps