Search code examples
kotlingsonretrofit2kotlin-multiplatform

Convert Retrofit Json to Kotlin Map


Ok, i have a problem in a Kotlin multiplatform project i am working on at the moment.

I want to get some Currency conversion rates from a api (http://www.floatrates.com/daily/usd.json) and save them in a local Database (Realm).

The problem i am currently facing is, that all the data in the JSON is saved in many many uniquely named objects and not in a Array (these objects all have the same structure).

{
"aud": {
    "code": "AUD",
    "alphaCode": "AUD",
    "numericCode": "036",
    "name": "Australian Dollar",
    "rate": 1.5120512400088,
    "date": "Wed, 13 Mar 2024 12:55:07 GMT",
    "inverseRate": 0.66135324884504
},
"cad": {
    "code": "CAD",
    "alphaCode": "CAD",
    "numericCode": "124",
    "name": "Canadian Dollar",
    "rate": 1.3495144747695,
    "date": "Wed, 13 Mar 2024 12:55:07 GMT",
    "inverseRate": 0.74100724274989
},
.
.
 // hundreds more
.
.
"inr": {
    "code": "INR",
    "alphaCode": "INR",
    "numericCode": "356",
    "name": "Indian Rupee",
    "rate": 82.850734958333,
    "date": "Wed, 13 Mar 2024 12:55:07 GMT",
    "inverseRate": 0.012069898963515
},
"jpy": {
    "code": "JPY",
    "alphaCode": "JPY",
    "numericCode": "392",
    "name": "Japanese Yen",
    "rate": 147.78881616883,
    "date": "Wed, 13 Mar 2024 12:55:07 GMT",
    "inverseRate": 0.0067664118701486
}
}

I want to save those currencies in some iterable Data structure (for example Map<String,Currency> ) with:

data class Currency(
   val code: String,
   val alphaCode: String,
   val numericCode: String,
   val name: String,
   val rate: Double,
   val date: String,
   val inverseRate: Double,
 )

But i cant get Retrofit to store the data into my map.

If i use the Json to kotlin class plugin for android studio i can generate a class like this:

data class AutoGeneratedResponseDataClass(
   val aed: Aed,
   val afn: Afn,
   val all: All,
   val amd: Amd,
   .
   .
    // hundreds more
   .
   .
   val xpf: Xpf,
   val yer: Yer,
   val zar: Zar,
   val zmw: Zmw,
)

with hundereds of other classes like these that are all exactly the same

 data class Aed(
    val alphaCode: String,
    val code: String,
    val date: String,
    val inverseRate: Double,
    val name: String,
    val numericCode: String,
    val rate: Double
)
.
.
.
data class Zmw(
    val alphaCode: String,
    val code: String,
    val date: String,
    val inverseRate: Double,
    val name: String,
    val numericCode: String,
    val rate: Double
)

I think technically it is possioble to iterate through all fields of an object by using reflection, but and i could change all those class types to one single class, but still, thats not really what i want.

To test if everything is working i have a Dialog that displays the json in form of a string and two buttons, one for a test api that works fine and one for this api that just returns "null", no matter what i try

Button(
onClick = { viewModel.getCurrencies() }
) {
    Text(text = "sync currencies")
}
Button(
onClick = { viewModel.getQuotes() }
) {
    Text(text = "sync quotes")
}
Button(
onClick = { viewModel.showDialog = true }
) {
    Text(text = "Show Response")
}

thats how it currently looks for testing

the code for the test api is:

//This code works and gets me the desired output
object RetrofitHelperQuotes {
    val baseUrl = "https://quotable.io/"

    fun getInstance(): Retrofit {
        return Retrofit.Builder().baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}



interface QuotesApi {
    @GET("/quotes")
    suspend fun getQuotes() : Response<QuoteList>
}
//with 
data class QuoteList(
    val count: Int,
    val lastItemIndex: Int,
    val page: Int,
    val results: List<Result>,
    val totalCount: Int,
    val totalPages: Int
)

data class Result(
    val _id: String,
    val author: String,
    val authorSlug: String,
    val content: String,
    val dateAdded: String,
    val dateModified: String,
    val length: Int,
    val tags: List<String>
)

// and this code in the viewmodel that gets called on button press
@OptIn(DelicateCoroutinesApi::class)
fun getQuotes(){
    val quotesApi = RetrofitHelperQuotes.getInstance().create(QuotesApi::class.java)
    GlobalScope.launch {
        val result = quotesApi.getQuotes()
        if (result != null)
//this is the string that gets stored in the viewModel and gets displayed onscreen when i press the third button
            jsonResponse = GsonBuilder().setPrettyPrinting().create().toJson(result.body())
    }
}

when i get the quotes from the test api

and the code for the api i actually want to use is:

object RetrofitHelperCurrency {
    val baseUrl = "http://www.floatrates.com/daily/"

    fun getInstance(): Retrofit {
        return Retrofit.Builder().baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }
}

interface CurrencyApi {

    @GET("/usd.json")
    suspend fun getCurrencies() : Response<AutoGeneratedResponseDataClass>

    @GET("/usd.json")
    suspend fun anotherTry() : Response<Map<String,Currency>>

    @GET("/usd.json")
    suspend fun yetAnotherTry() : Response<CurrencyApiResponse>
}
//with
data class CurrencyApiResponse(
    val data: Map<String,Currency>,
)
// and the rest of my classes as described at the top

// and this code in the viewmodel that gets called on button press
@OptIn(DelicateCoroutinesApi::class)
fun getCurrencies(){
    val currencyApi = RetrofitHelperCurrency.getInstance().create(CurrencyApi::class.java)
    GlobalScope.launch {
        val result = currencyApi.getCurrencies()
        if (result != null)
//this is the string that gets stored in the viewModel and gets displayed onscreen when i press the third button
            jsonResponse = GsonBuilder().setPrettyPrinting().create().toJson(result.body())

    }
}

when i try to get my currencies

After all that is resolved i want to store the data in a local realm database in case i lose internet connection or something like that, but i am currently stuck at this problem and cant get it to work.

In general i am kina new to programming and this is jsut a "small" learning project for me, but even after googeling my ass off and trying countless things i cant get it to work, so any help is appreciated.


Solution

  • Try removing the "/" in front of front of "/usd.json".

    Using "/usd.json" results in the following URI: "http://www.floatrates.com/usd.json". But the correct one is "http://www.floatrates.com/daily/usd.json". Tested with wireshark.

    The following worked for me to extract the result into a currency list:

    import retrofit2.Call
    import retrofit2.Retrofit
    import retrofit2.converter.gson.GsonConverterFactory
    import retrofit2.http.GET
    
    interface CurrencyService {
        @GET("usd.json")
        fun getUsd(): Call<Map<String, Currency?>?>?
    }
    
    data class Currency(
        val code: String,
        val alphaCode: String,
        val numericCode: String,
        val name: String,
        val rate: Double,
        val date: String,
        val inverseRate: Double
    )
    
    fun main() {
        val retrofit = Retrofit.Builder()
            .baseUrl("http://www.floatrates.com/daily/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    
        val service = retrofit.create(CurrencyService::class.java)
    
        val currencyMap = service.getUsd()?.execute()?.body()
        val currencies = currencyMap?.values
        println("repos: ${currencies}")
    }