May the power of Coroutine be with you…

Gaurang Shaha
Globant
Published in
4 min readApr 13, 2020

Using Coroutine to get rid of callbacks & asynchronous programming with ease.

Kotlin’s coroutines introduce a new style of concurrency that can be used on Android to simplify asynchronous code. It was introduced in Kotlin in 1.3, and since then I have been using it extensively for doing asynchronous programming. Along the way, I have learned a few use cases, in a way it surprised me a lot. Basic knowledge of coroutine is a prerequisite for understanding the following concepts.

Convert a callback to a suspended function

Kotlin provides a nice and fluent API to convert a callback to a suspended function. Kotlin provides a builder SuspendCancellableCoroutine which will act as an adapter between the coroutine world and the callback-based world.

It will need a lambda as input, which will be executed and immediately suspended. cancellableContinuation will be provided, using which we can resume the execution at a later point in time.

Resumes the execution of the corresponding coroutine passing [value] as the return value of the last suspension point.

Resumes the execution of the corresponding coroutine so that the [exception] is re-thrown right after the last suspension point.

Cancels this continuation with an optional cancellation cause. The result is true if this continuation was cancelled as a result of this invocation, and false otherwise.

I was using MVVM architecture and decided that the repository would have suspended functions only. Which can be further called from ViewModel in the viewModelScope. It worked great for the Room database and Retrofit library as they support coroutine out of the box.

I came across a scenario where I needed the location update. I have decided to write it in the repository as it is simply an input from another sensor in mobile devices. As per the standard practice, I was using FusedLocationAPI to get the location details needed.

As per my rule, the repository will have only suspended functions. This constraint forced me to check for other alternatives and I came across this builder.

Getting the last known location

The first goal was to create a suspend function that can return the last known location of the device if it is available or will throw the appropriate error. FusedLocationAPI provides a method getLastLocation() which will return the task containing either location or exception. We can add a complete listener to this task to observe the result.

To convert this callback-based function into the suspended function, I have used SuspendCancellableCoroutine as shown below.

suspend fun getLastKnownLocation(): Location? {
return suspendCancellableCoroutine { continuation ->
val flp = LocationServices.getFusedLocationProviderClient(context)
val task = flp.lastLocation
task.addOnCompleteListener {
if (it.isSuccessful)
continuation.resume(task.result)
else
continuation.resumeWithException(it.exception ?: NullPointerException())
}
}
}

Checking the GPS settings of the device

SettingAPI must be accessed to ensure that the device’s system settings are properly configured for the app’s location needs. When requesting location services, the device’s system settings may be in a state that prevents an app from obtaining the location data that it needs. For example, GPS or Wi-Fi scanning may be switched off. This intent makes it easy to:

  • Determine if the relevant system settings are enabled on the device to carry out the desired location request.
  • Optionally, invoke a dialog that allows the user to enable the necessary location settings with a single tap.

To convert this into the suspended function, I have used the following code

suspend fun checkLocationSettings(locationRequest: LocationRequest): LocationSettingResult {
return suspendCancellableCoroutine { continuation ->
val builder = LocationSettingsRequest.Builder()
.addLocationRequest(locationRequest)
val client: SettingsClient = LocationServices.getSettingsClient(context)
val task: Task<LocationSettingsResponse> = client.checkLocationSettings(builder.build())
task.addOnSuccessListener {
continuation.resume(LocationSettingResult(true))
}
task.addOnFailureListener {
continuation.resume(LocationSettingResult(false, it))
}
}
}
data class LocationSettingResult(val success: Boolean, val error: Exception? = null)

Here purposefully I am not calling the resumeWithException() method in failure callback because instead of abruptly canceling the execution, I want to show a location setting popup.

Fetching continuous location updates

As the last part of the use case, I want to get a continuous location update from FusedLocationAPI. Before fetching the location updates I will check the location setting, if everything goes well I will start the updates.

suspend fun getLocationUpdates(
onLocationUpdated: (Pair<Double, Double>?, error: Exception?) -> Unit) {
return coroutineScope {
val request = LocationRequest.create().apply {
interval = 10000
fastestInterval = 2000
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}

val locationSettingsResult = checkLocationSettings(request)

if (locationSettingsResult.success) {
val done = CompletableDeferred<Unit>()
val flp = LocationServices.getFusedLocationProviderClient(context)

val callback = object : LocationCallback() {
val mutex = Mutex()
var job: Job? = null
override fun onLocationResult(locationRestult: LocationResult?) {
locationRestult?.lastLocation?.let {
job?.cancel()
job = launch {
mutex.withLock {
onLocationUpdated(Pair(it.latitude, it.longitude), null)
}
}
}
}
}
try {
flp.requestLocationUpdates(request, callback, Looper.getMainLooper())
done.await()
} finally {
flp.removeLocationUpdates(callback)
}
} else {
onLocationUpdated(null, locationSettingsResult.error)
}
}

getLocationUpdates() function takes input as a lambda which will be called when either location or error is received. Let’s consider a scenario where the GPS setting is not appropriate, it needs to be turned on, and then getLocationUpdates() will get called with an exception. By calling the method mentioned in SettingsAPI up for location enable can be shown.

On the other hand, if the GPS setting is appropriate then getLocationUpdates() will be called with Pair<Double, Double> containing latitude and longitude.

getLocationUpdates() function will get called from viewModelScope, so until the ViewModel is in memory location update will be delivered. The coroutine supports the structural concurrency due to which if the viewModelScope expires, it will throw the exception which will be caught by finally block and updates will be stopped. In this way, we can guarantee there is no memory leak happens during execution.

Using this builder function we can easily bridge the gap between callback-based and coroutine-based worlds. This will also work with any kind of Future, Task, Single, Maybe, etc. objects as they internally use the callbacks.

--

--