Android Fintech Guide

Kotlin Android Sukuk Tracker

Fetch curated GCC Sukuk records with Retrofit, map the JSON response to Kotlin data classes, and display issuer, profit rate, and maturity in a selectable Compose dashboard.

1. Configure the server-side API client

Create a KhaleejiAPI key with the finance:sukuk scope and store it as the server environment variable KHALEEJI_API_KEY. Send it in the Authorizationheader using the Bearer scheme. This Retrofit client and key belong in your Kotlin backend, not in the Android app. The backend endpoint shown below is an app-specific proxy; authenticate users, enforce access and rate limits, and return the upstream JSON envelope without exposing the API key.

kotlin
dependencies {
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7")
}

Model the Sukuk response

A list response contains data.sukuk. Gson ignores additional response fields, so these classes can select only what the dashboard needs.

kotlin
data class SukukEnvelope(val data: SukukData)
data class SukukData(val sukuk: List<Sukuk>)
data class Sukuk(
val id: String,
val name: String,
val issuer: String,
val country: String,
val countryCode: String,
val currency: String,
val couponRate: Double,
val maturityDate: String,
val status: String,
)

Preserve Gson models in Android release builds

Gson reads these DTO fields by reflection. Add the following rules to the Android app module's proguard-rules.pro to prevent R8 from renaming or removing all three DTOs and their fields. The ** wildcard matches your package; you can replace it with each class's actual package to narrow the rules.

text
-keep class **.SukukEnvelope { *; }
-keep class **.SukukData { *; }
-keep class **.Sukuk { *; }

In the app module's build.gradle.kts, ensure the minified release build loads that rules file (merge this into your existing release configuration). Test a minified release build with a known non-empty response and verify issuer, coupon rate, and maturity values. Missing Gson mappings can otherwise appear as an empty dashboard.

kotlin
android {
buildTypes {
getByName("release") {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}

Call the KhaleejiAPI endpoint from your backend

kotlin
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface KhaleejiSukukService {
@GET("api/v1/sukuk/list")
suspend fun listSukuk(
@Header("Authorization") authorization: String,
@Query("country") country: String? = null,
@Query("status") status: String? = null,
): Response<SukukEnvelope>
}
private const val BEARER_SCHEME = "Bearer"
val khaleejiSukukService = Retrofit.Builder()
.baseUrl("https://khaleejiapi.dev/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(KhaleejiSukukService::class.java)
suspend fun fetchActiveUaeSukuk(): SukukEnvelope {
val apiKey = System.getenv("KHALEEJI_API_KEY")
?: error("Missing server-side KHALEEJI_API_KEY")
val response = khaleejiSukukService.listSukuk(
authorization = BEARER_SCHEME + " " + apiKey,
country = "AE",
status = "active",
)
if (!response.isSuccessful) {
error("Sukuk API returned HTTP ${response.code()}")
}
return response.body()
?: error("Sukuk API returned an empty response")
}

2. Fetch data from Android through your backend

Replace the example host and route with your authenticated backend endpoint. Pass the signed-in user's session token—not the KhaleejiAPI key—in the Authorization header. This user token is separate from the server's KhaleejiAPI key. Keep tokens out of logs and request URLs.

kotlin
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Query
interface SukukBackendService {
@GET("api/mobile/sukuk")
suspend fun listSukuk(
@Header("Authorization") authorization: String,
@Query("country") country: String = "AE",
@Query("status") status: String = "active",
): Response<SukukEnvelope>
}
private const val BEARER_SCHEME = "Bearer"
val retrofit = Retrofit.Builder()
.baseUrl("https://api.yourcompany.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
class SukukRepository(
private val service: SukukBackendService =
retrofit.create(SukukBackendService::class.java),
) {
suspend fun activeUaeSukuk(sessionToken: String): List<Sukuk> {
val authorization = BEARER_SCHEME + " " + sessionToken
val response = service.listSukuk(authorization)
if (!response.isSuccessful) {
error("Your Sukuk service returned HTTP ${response.code()}")
}
return response.body()?.data?.sukuk.orEmpty()
}
}

Load data in a ViewModel

kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
data class SukukUiState(
val loading: Boolean = false,
val sukuk: List<Sukuk> = emptyList(),
val error: String? = null,
)
class SukukViewModel(
private val repository: SukukRepository,
) : ViewModel() {
private val _state = MutableStateFlow(SukukUiState())
val state: StateFlow<SukukUiState> = _state.asStateFlow()
fun load(sessionToken: String) {
viewModelScope.launch {
_state.value = SukukUiState(loading = true)
_state.value = try {
SukukUiState(sukuk = repository.activeUaeSukuk(sessionToken))
} catch (cancelled: CancellationException) {
throw cancelled
} catch (_: Exception) {
SukukUiState(error = "Sukuk data is temporarily unavailable.")
}
}
}
}

3. Display selectable Sukuk cards

Collect viewModel.state in your screen and pass the state here. The selected record can open a detail screen or update the dashboard's selected-instrument panel.

kotlin
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun SukukDashboard(
state: SukukUiState,
onSelect: (Sukuk) -> Unit,
) {
when {
state.loading -> CircularProgressIndicator()
state.error != null -> Text(state.error)
else -> LazyColumn {
items(state.sukuk, key = { it.id }) { sukuk ->
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
onClick = { onSelect(sukuk) },
) {
Column(Modifier.padding(16.dp)) {
Text(sukuk.name, style = MaterialTheme.typography.titleMedium)
Text("Issuer: ${sukuk.issuer}")
Text("Profit rate (coupon): ${sukuk.couponRate}%")
Text("Maturity: ${sukuk.maturityDate}")
Text("${sukuk.currency} · ${sukuk.status}")
}
}
}
}
}
}