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.
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.
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.
-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.
android { buildTypes { getByName("release") { isMinifyEnabled = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } }}Call the KhaleejiAPI endpoint from your backend
import retrofit2.Responseimport retrofit2.Retrofitimport retrofit2.converter.gson.GsonConverterFactoryimport retrofit2.http.GETimport retrofit2.http.Headerimport 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.
import retrofit2.Responseimport retrofit2.Retrofitimport retrofit2.converter.gson.GsonConverterFactoryimport retrofit2.http.GETimport retrofit2.http.Headerimport 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
import androidx.lifecycle.ViewModelimport androidx.lifecycle.viewModelScopeimport kotlinx.coroutines.CancellationExceptionimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.StateFlowimport kotlinx.coroutines.flow.asStateFlowimport 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.
import androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.foundation.lazy.LazyColumnimport androidx.compose.foundation.lazy.itemsimport androidx.compose.material3.Cardimport androidx.compose.material3.CircularProgressIndicatorimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dp @Composablefun 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}") } } } } }}couponRate is the API's annual contractual profit-rate reference, not a live market yield or yield-to-maturity. The endpoint does not provide live prices or calculate investment returns. maturityDate is a string and may be Perpetual; display it as text rather than parsing every value as a date. Records are curated reference data, so verify financial information before making investment decisions.