1
votes

I have the following code which makes an API call to get Addresses from Postcode.

fun getAddressFromPostCode(postCode: String): List<Address>{

    val trimmedPostCode = postCode.replace("\\s".toRegex(),"").trim()
    val dataBody = JSONObject("""{"postcode":"$trimmedPostCode"}""").toString()
    val hmac = HMAC()
    val hmacResult = hmac.sign(RequestConstants.CSSecretKey, dataBody)
    val body = JSONObject("""{"data":$dataBody, "data_signature":"$hmacResult"}""")


    val url = RequestConstants.URL

    val retrofit: Retrofit = Retrofit.Builder()
        .baseUrl(url)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val api:GetAddressAPIService = retrofit.create(GetAddressAPIService ::class.java)
    var myList = emptyList<Address>()
    val myCall: Call<GetAddressResponse> = api.getAddress(body)

    myCall.enqueue(object : Callback<GetAddressResponse> {
        override fun onFailure(call: Call<GetAddressResponse>?, t: Throwable?) {
            Log.d("RegistrationInteractor", "Something went wrong", t)
            Log.d("RegistrationInteractor", call.toString())
        }

        override fun onResponse(call: Call<GetAddressResponse>?, response: Response<GetAddressResponse>?) {
            // Success response
            myList = response!!.body()!!.addresses
        }

    })
    return myList
}

And here's where I make the call:

interface GetAddressAPIService {
@Headers("Accept: application/json; Content-Type: application/json")
@POST("postcode_search.php")
    fun getAddress(@Body dataBody: JSONObject): Call<GetAddressResponse>

} The GetAddressResponse looks like this and seems to be correct: data class GetAddressResponse( val success: Int, val addresses: List )

The databody is {"data":{"postcode":"M130EN"},"data_signature":"long data signature"} and when I run this in Postman I get a list of addresses, but when I run it in the app I get a 200 OK response but no addresses. Any ideas why?

2
If your request is fine, then the issue is that the method is async but you always return an empty list. I solved this issue in the next post stackoverflow.com/questions/60446998/… There are more options, but in your case, you could to use an interface and avoid the livedata, or use livedata too, suspend func...Manuel Mato
Check GetAddressResponse. Does it has correct structure?Nostradamus
Does this answer your question? Convert callback hell to deferred objectcutiko

2 Answers

2
votes

Your code is returning but enqueue is async so that is not guaranteed to happen before the return. If you read the enqueue there is a callback there, which means it is gonna call back after is ready, since it is an HTTP request then that takes some time and it is finished after the return.

You have 2 alternatives, add the Callback<GetAddressResponse> as an argument or any other callback as an argument, or you can make it suspend.

Callback

Coroutines are the recommended way of doing things now so this is no longer considered a good practice.

fun getAddressFromPostCode(postCode: String, callback: Callback<GetAddressResponse): List<Address>{
    ...
    myCall.enqueue(callback)

}

The calling class has to implement the callback and pass it as an argument. You can make that more kotlin way by using a lambda:

fun getAddressFromPostCode(postCode: String, callback: (items: List<Address> -> Unit))
    ...
            override fun onResponse(call: Call<GetAddressResponse>?, response: Response<GetAddressResponse>?) {
            callback(response...)
        }

}

So that makes the calling class use it this way

yourClass.getAddressFromPostCode(postalCode) { -> items
    //do your thing with items
}

Coroutines

You can transform it to linear code by using suspend functions:

//make it return optional to know if it was failure
suspend fun getAddressFromPostCode(postCode: String): List<Address>? {
    ...
    return try {
        myCall.invoke().result?.body()
    } catch (e: Exception) {
        null
    }

}

And then the calling class has to use it like this:

    lifeCycleScope.launch {
        val addresses = yourClass.getAddressFromPostCode(postalCode)
        //do your thing on the UI with the addresses maybe pass it to an adapter
    }

0
votes

The problem you're facing here is asynchronously data fetching, while returning immediately from function.

In code you provided, myCall.enqueue call is asynchronous and once it's done, it invokes onFailure or onResponse, depending if the request is failed or not. On the last line of getAddressFromPostCode function, you return myList value, which is still empty in that case, because onResponse has not been called yet.

I can suggest to get familiar with coroutines, which would make this code easier to reason about, while handling asynchronous work. Or you could simply provide a callback function to getAddressFromPostCode to be called once the request is done or failed.