I have an issue with Kotlin and coroutines. I've written this method to call an api and get some information from this. I call calculateDistance, which works out the distances for me in KM. The issue that I have is that the UI loads before this method is completed. I've been trying to work out a way around this using coroutines, however, I seem to be coming stuck.
This function returned String is then used to render in an Activity.
Thanks
private fun getPostcodeLocation(listOfPostCodes: List<Pair<Boolean, String>>): String {
val jsonList = JSONObject()
jsonList.put("postcodes", JSONArray(listOfPostCodes.map { it.second}))
val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonList.toString())
val request = okhttp3.Request.Builder().url("https://api.postcodes.io/postcodes" ).post(body).build()
val client = OkHttpClient()
client.newCall(request).enqueue(object: Callback {
override fun onResponse(call: Call, response: Response) {
val responseBody = response.body()?.string()
val jsonTree = JsonParser().parse(responseBody).asJsonObject
val resultJsonObj = jsonTree.asJsonObject.getAsJsonArray("result").asJsonArray.iterator()
resultJsonObj.forEach {
val resultObject = Gson().toJsonTree(it).asJsonObject.getAsJsonObject("result")
val lat = resultObject.get("latitude").asDouble
val lon = resultObject.get("longitude").asDouble
listOfLonLat.add(PostcodeLongLat(lon, lat))
}
distance = calculateDistance(listOfLonLat)
Log.d("PostCodeChecker", "This is the distance inside the callback $distance")
}
override fun onFailure(call: Call, e: IOException) {
println(call)
println(e)
Log.d("PostCodeChecker", "Failed to get the information from the API")
distance = "Not Available"
}
})
Log.d("PostCodeChecker", "This is the distance $distance")
return distance
}
Expected a string representing the distance between two long/lat points on a map, get Null.
this doesn't seem like a coroutines-related question. Your api call is happening asynchronously, hence you can't expect to get the result before returning distance. What you can do is access the UI element you need in onResponse method. E.g.
override fun onResponse(call: Call, response: Response) {
//parsing, calculations etc.
yourTextView.text = distance.toString
}
Related
I have been trying to create a decorator for a lambda function in Kotlin. The requirements for the decorator are following:
The decorator must be able to REPEAT the process passed in the form of lambda function
The decorator must be able to TIMEOUT after a certain time which will be passed along in the decorator.
This is what I have been able to come up so far:
inline fun <T, R> testDecorator(input: T,
timesToRepeat: Int,
timeout: Long,
crossinline work: (T) -> R): R {
repeat(timesToRepeat - 1) {
try {
return runBlocking {
withTimeout(timeout) {
work(input)
}
} catch (ex: Exception) {
// Deal with exception
}
}
return work(input)
}
Right now, the work function will only be making a http request but later on I plan to add some more work.
Sample work:
fun sampleWork() {
val client = HttpClient.newBuilder().build();
val request = HttpRequest.newBuilder() .uri(URI.create("some url")) .build();
val response = client.send(request, HttpResponse.BodyHandlers.ofString());
// Parse respone
return parsedResponse;
}
The problem is that the timeout is not working as expected and the function only ends after all the processes in the lambda have ended. Can someone tell me what am I missing here?
I am trying to convert my pet app to use coroutines instead of callbacks.
I am half way, but I cannot see how to get around the callback I have in this function. Is there a way to use async to get rid of callbacks or am I climbing the wrong tree?
This is what I have so far:
const val url = "https://pokeapi.co/api/v2/pokemon/"
class PokeClient {
fun getPokemonData(context: Context, successCallBack: (Pokemon) -> Unit, pokemonName: String) = runBlocking {
val queue = Volley.newRequestQueue(context)
val request = url.plus(pokemonName)
var deferredResult = async {
val stringRequest = StringRequest(Request.Method.GET, request, Response.Listener<String> { response ->
val jObj = JSONObject(response)
val imgUrl = jObj
.getJSONObject("sprites")
.getJSONObject("other")
.getJSONObject("official-artwork")
.getString("front_default")
val inputStream = URL(imgUrl).openStream()
successCallBack(Pokemon(name = jObj.getString("name"), image = BitmapFactory.decodeStream(inputStream)))
}, Response.ErrorListener {
val toast = Toast.makeText(context, "error talking to professor Oak!", Toast.LENGTH_SHORT)
toast.show()
})
queue.add(stringRequest)
}
deferredResult.await()
}
}
Any ideas?
Thank you,
Android Newbie
Essentially you need to convert network call with callback code block into a suspending function which can be called from any coroutine, this can be done using suspendCoroutine function, it basically provides you with a continuation object, which can be used to return data from inside the Response callbacks in your case
suspend fun getPokemon() = suspendCoroutine<Pokemon> { cont ->
val queue = Volley.newRequestQueue(this)
val url = "https://pokeapi.co/api/v2/pokemon/"
val stringRequest = StringRequest(Request.Method.GET, url,
Response.Listener<Pokemon> { response ->
val jObj = JSONObject(response)
val imgUrl = jObj.getJSONObject("sprites")
.getJSONObject("other")
.getJSONObject("official-artwork")
.getString("front_default")
val inputStream = URL(imgUrl).openStream()
/* call continuation.resume and pass your object */
cont.resume(Pokemon(name = jObj.getString("name"), image = BitmapFactory.decodeStream(inputStream)))
},
Response.ErrorListener {
/* if network call fails then post appropriate error */
cont.resumeWithException(YourExceptoin)
})
queue.add(stringRequest)
}
Now you can call this function from a coroutine and get a Pokemon as following
runBlocking{
try { val pokeMon = getPokemon() }
catch(e: Exception) { Log.d(TAG, "Cant get pokemon") }
}
Note : its OK to use runBlocking only for learning and exploration otherwise its not a good idea, use launch or async
Edit : As noted in comment you can also use suspendCancellableCoroutine if you need to support cancellation (which you should for structured concurrency).
using kotlin, having code
fun fetchRemoteDataApi(): Single<RemoteDataResponse> = networkApi.getData()
// it is just a retrofit
#GET(".../api/getData")
fun getData() : Single<RemoteDataResponse>
fun mergeApiWithDb(): Completable = fetchRemoteDataApi()
.zipWith(localDao.getAll())
.flatMapCompletable { (remoteData, localData) ->
doMerge(remoteData, localData) //<== return a Completable
}
the code flow:
val mergeApiDbCall = mergeApiWithDb().onErrorComplete().cache() //<=== would like do some inspection at this level
PublishSubject.create<Unit>().toFlowable(BackpressureStrategy.LATEST)
.compose(Transformers.flowableIO())
.switchMap {
//merge DB with api, or local default value first then listen to DB change
mergeApiDbCall.andThen(listAllTopics())
.concatMapSingle { topics -> remoteTopicUsers.map { topics to it } }
}
.flatMapCompletable { (topics, user) ->
// do something return Completable
}
.subscribe({
...
}, { throwable ->
...
})
and when making the call
val mergeApiDbCall = mergeApiWithDb().onErrorComplete().cache()
the question is if would like to inspect on the Singles<RemoteDataResponse> returned from fetchRemoteDataApi() (i.e. using Log.i(...) to printout the content of RemoteDataResponse, etc.), either in got error or success case, how to do it?
/// the functions
fun listAllTopics(): Flowable<List<String>> = localRepoDao.getAllTopics()
// which a DAO:
#Query("SELECT topic FROM RemoteDataTable WHERE read = 1")
fun getAllTopics(): Flowable<List<String>>
///
private val remoteTopicUsers: Single<List<User>>
get() {
return Single.create {
networkApi.getTopicUsers(object : ICallback.IGetTopicUsersCallback {
override fun onSuccess(result: List<User>) = it.onSuccess(result)
override fun onError(errorCode: Int, errorMsg: String?) = it.onError(Exception(errorCode, errorMsg))
})
}
}
You cannot extract information about elements from the Completable. Though you can use doOnComplete() on Completable, it will not provide you any information about the element.
You can inspect elements if you call doOnSuccess() on your Single, so you need to incorporate this call earlier in your code. To inspect errors you can use doOnError() on both Completable or Single.
My function is quite straightforward,
Main Thread: Initializes a variable ->
Background Thread: Fire network request, assign the result back to the previous variable ->
Main Thread: Display that variable
Code below:
suspend fun createCity(context: Context, newCity: MutableLiveData<NewIdea>, mapBody: Map<String, String>, token: String) {
lateinit var response: NewIdea
try {
withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
response = webservice.createIdea(tripId, map, "Bearer $token")
getTrip(context, token)
}
} catch (e: Exception) {
Log.e(TAG, e.message)
}
newCity.value = response
}
But sometimes (it only happened 2 times actually) crashlytics reports crash for this line newCity.value = response
Fatal Exception: kotlin.UninitializedPropertyAccessException: lateinit property response has not been initialized
I don't really understand how that can happen.
Is this the correct way to return value from coroutine function?
thanks
Well if try block fails, it might happen that the lateinit variable isn't set at all. You should put the ui update code inside the try block as well, and handle the Exception separately:
Sidenote: withContext is well-optimized to return values, so you can make use of it.
suspend fun createCity(context: Context, newCity: MutableLiveData<NewIdea>, mapBody: Map<String, String>, token: String) {
try {
val response: NewIdea = withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
// does createIdea() first store it in var, then does getTrip(), then returns the result of createIdea() stored previously
webservice.createIdea(tripId, map, "Bearer $token").also { getTrip(context, token) } // ^withContext
}
newCity.value = response
} catch (e: Exception) {
Log.e(TAG, e.message)
}
}
A quick tip (optional): You can wrap the UI updating code with a withContext that dispatches the work to Dispatchers.Main when not running in main thread, while if running in main do nothing:
withContext(Dispatchers.Main.immediate) {
val response: NewIdea = withContext(Dispatchers.IO) {
val map = generateRequestBody(mapBody)
// does createIdea() first store it in var, then does getTrip(), then returns the result of createIdea() stored previously
webservice.createIdea(tripId, map, "Bearer $token").also { getTrip(context, token) } // ^withContext
}
newCity.value = response
}
I have the following method which simply fetches the data in ether SYNC or ASYNC way:
enum class CallType { SYNC, ASYNC }
suspend fun get( url: String, callType : CallType, mock : String? = null, callback: Callback? ): Response?
{
var response : okhttp3.Response ?= null
val request = Request.Builder()
.url( url )
.build()
val call = client.newCall( request )
if( mock != null )
{
// this works fine for SYNC, but how to make it work with ASYNC callback?
delay( 1000 )
return okhttp3.Response.Builder().body(
ResponseBody.create( "application/json".toMediaType(), mock )
).build()
}
if( callType == CallType.ASYNC && callback != null )
call.enqueue( callback )
else
response = call.execute()
return response
}
I would like to be able to mock/overwrite the response. I can do this fine when doing it the SYNC way, since I simply have to construct and return a fake okhttp3.response, like the snippet below, and the code execution stops and everything works out great:
if( mock != null )
{
delay( 1000 )
return okhttp3.Response.Builder().body(
ResponseBody.create( "application/json".toMediaType(), mock )
).build()
}
The problem is that I would like to be able to do the same for ASYNC calls, but I'm not sure where to go from here. I'm basically trying to replicate the enqueue() method so that after some delay my callback gets triggered (which was passed to the get() method) and my fake okhttp3.Response is returned via the callback, instead of return. Any suggestions on how to accomplish this? Thanks!
You're mixing different concepts with your implementation. Asynchrony should be controlled with the CoroutineContext instead of parameters. Like this you'll always return a non-null value. Also it's wise to hide the implementation details (here OkHttp) and not expose it.
You could use suspendCoroutine to bridge OkHttp with the couroutine.
suspend fun get(
url: String,
mock : String? = null
) = if(mock != null) {
delay( 1000 )
Response.Builder().body(
ResponseBody.create(
"application/json".toMediaType()
mock
)
).build()
} else suspendCoroutine { continuation ->
client.newCall(
Request.Builder()
.url(url)
.build()
).enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) =
continuation.resumeWithException(e)
override fun onResponse(call: Call, response: Response) =
continuation.resume(response)
}
)
}
To access it synchronously just use
runBlocking { get(url, mock) }
If you really need to provide your own Callable, you could easily delegate to it. But you'd also have to create a call, even though you wouldn't need it when you're mocking the response.
one easy way is to just call the callback in a synchronous way:
if (mock != null) {
val response = ... // prepare mock response here
callback.onResponse(response)
}
in consequence the callback would be invoked even before your get function finishes.
If you want to achieve that the response actually is delivered asynchronously you need to execute the mock delivery from an extra coroutine
if (mock != null) {
GlobalScope.launch {
val response = ... // prepare mock response here
delay(1000)
callback.onResponse(response)
}
}