Android kotlin task to be executed using coroutines - android

As an example, I'm using FusedLocationProviderClient to access the current location, which returns a task which callback will eventually return the location. The method looks something like follows:
fun getLocation(callback: MyCallback){
val flpc = LocationServices.getFusedLocationProviderClient(it)
flpc.lastLocation.addOnSuccessListener {
callback.onLocation(it)
}
}
Is it possible to transform this so that I can use corroutines to suspend this function and wait for the task returned by flpc.lastLocation so I can return it in this method and this way get rid of that callback? For example something like this:
suspend fun getLocation(): Location? =
withContext(Dispachers.IO){
val flpc = LocationServices.getFusedLocationProviderClient(it)
return#withContext flpc.lastLocation.result()
}
My question is if there is something around coroutines where I can return the result of a Task (in this example, a Task<Location>)
Thanks in advance!

The kotlinx-coroutines-play-services library has a Task<T>.await(): T helper.
import kotlinx.coroutines.tasks.await
suspend fun getLocation(): Location? =
LocationServices.getFusedLocationProviderClient(context).lastLocation.await()
Alternatively take a look at Blocking Tasks
It would be used the next way:
suspend fun getLocation(): Location? =
withContext(Dispachers.IO){
val flpc = LocationServices.getFusedLocationProviderClient(context)
try{
return#withContext Tasks.await(flpc.lastLocation)
catch(ex: Exception){
ex.printStackTrace()
}
return#withContext null
}
Just to add to this example, for completion purposes, the call to getLocation() would be done the next way:
coroutineScope.launch(Dispatchers.Main) {
val location = LocationReceiver.getLocation(context)
...
}
However this negates the benefits of coroutines by not leveraging the available callback and blocking a thread on the IO dispatcher and should not be used if the alternative is available.

Another way that I have done this that can also be used with any callback type interface is to use suspendCoroutine<T> {}.
So for this example it would be:
suspend fun getLocation(): Location? {
return suspendCoroutine<Location?> { continuation ->
val flpc = LocationServices.getFusedLocationProviderClient(it)
flpc.lastLocation.addOnSuccessListener { location ->
continuation.resume(location)
}
// you should add error listener and call 'continuation.resume(null)'
// or 'continuation.resumeWith(Result.failure(exception))'
}
}

Related

Return value from suspend function when other flow is completed

I have two suspend functions which are callbackFlow. I call one of them from suspend function which return String. I want to wait for location from getLocation() in serializeEvent() function and after getting value return string.
suspend fun getLocation(applicationContext: Context) = callbackFlow {
val locationProvider = LocationServices.getFusedLocationProviderClient(applicationContext)
if (isLocationPermissionGranted(applicationContext)) {
locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
.addOnSuccessListener {
if (it != null) {
trySend(it)
} else {
launch { trySend(getLastKnownLocation(locationProvider)) }
}
}
}
}
private suspend fun getLastKnownLocation(locationProvider: FusedLocationProviderClient) = callbackFlow {
runCatching {
locationProvider.lastLocation
.addOnSuccessListener { trySend(it) }
.addOnCanceledListener { trySend(null) }
.addOnFailureListener { trySend(null) }
}.onFailure {
trySend(null)
}
awaitClose { this.cancel() }
}
How I can return string when I get value from getLastKnownLocation()
suspend fun serializeEvent(eventJson: JSONObject): String {
CoroutineScope(Dispatchers.Default).launch {
LocationProvider.getLocation(getApplicationContext!!).collect {
}
}
//some code here
return eventJson.toString()
}
It doesn't make sense to turn a Google Task into a Flow using callbackFlow, because a Task only produces one thing, not a series of things. Typically, the way to convert for coroutines a callback that returns only once is to convert it into a suspend function using suspendCoroutine or suspendCancellableCoroutine.
However, a Task.await() extension suspend function is already provided so you can use it synchronously with coroutines. (Make sure you're using the -ktx version of the location library.) Instead of using listeners, you surround it with try/catch.
It seems like you just want to return null if no location is available, so here's how I would rewrite that function:
suspend fun getLocationOrNull(applicationContext: Context): Location? {
if (!isLocationPermissionGranted(applicationContext)) {
return null
}
val locationProvider = LocationServices.getFusedLocationProviderClient(applicationContext)
return try {
locationProvider.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
.await() ?: locationProvider.lastLocation.await()
} catch (e: Exception) {
Log.w(TAG, "Failed to retrieve current location")
null
}
}
In your usage site code, it doesn't make sense that you're launching a coroutine from within the suspend function. If you want to do something and wait for it in a suspend function, you can just do it directly without firing off a new coroutine in some other CoroutineScope.
suspend fun serializeEvent(eventJson: JSONObject): String {
val knownLocation: Location? =
LocationProvider.getLocation(getApplicationContext!!)
//some code here. Do something with knownLocation, which might be null
return eventJson.toString()
}
Side note, it looks like a code smell to me that you have to use !! when getting your application context. An Application Context must always exist, so it shouldn't need to be a nullable property.
.addOnSuccessListener { trySend(it) }

Kotlin withTimeout coroutine cancellation

I'm trying for the first time the coroutine function withTimeout. I'm trying to fetch the current location from the device GPS in Android, and add a timeout in case no location is available. I'm not controlling the process of fetching a location so I cannot make it cancellable easily.
Update: I ended up with a custom timeout logic as the android native api is not cancellable:
suspend fun LocationManager.listenLocationUpdate(): Location? =
withTimeoutOrNull(TIMEOUT) {
locationManager.listenLocationUpdate("gps")
}
private suspend fun LocationManager.listenLocationUpdate(provider: String) =
suspendCoroutine<Location?> { continuation ->
requestLocationUpdates(provider, 1000, 0f, object: TimeoutLocationListener{
override fun onLocationChanged(location: Location?) {
continuation.resume(location)
this#listenLocationUpdate.removeUpdates(this)
}
})
}
So the process of requesting a location belongs to the sdk and I cannot make it cancellale easily. Any suggestion?
For withTimeout[OrNull] to work, you need a cooperative cancellable coroutine. If the function you call is blocking, it will not work as expected. The calling coroutine will not even resume at all, let alone stop the processing of the blocking method. You can check this playground code to confirm this.
You have to have a cancellable API in the first place if you want to build coroutine-based APIs that are cancellable. It's hard to answer your question without knowing the exact function you're calling, though.
With Android's LocationManager, you can for instance wrap getCurrentLocation into a cancellable suspending function (this function is only available in API level 30+):
#RequiresApi(Build.VERSION_CODES.R)
#RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
suspend fun LocationManager.getCurrentLocation(provider: String, executor: Executor): Location? = suspendCancellableCoroutine { cont ->
val signal = CancellationSignal()
getCurrentLocation(provider, signal, executor) { location: Location? ->
cont.resume(location)
}
cont.invokeOnCancellation {
signal.cancel()
}
}
Otherwise you could also use callbackFlow to turn the listener-based API into a cancellable Flow-based API which unsubscribes upon cancellation (by removing the listener):
#OptIn(ExperimentalCoroutinesApi::class)
#RequiresPermission(anyOf = [permission.ACCESS_COARSE_LOCATION, permission.ACCESS_FINE_LOCATION])
fun LocationManager.locationUpdates(provider: String, minTimeMs: Long, minDistance: Float = 0f): Flow<Location> =
callbackFlow {
val listener = LocationListener { location -> sendBlocking(location) }
requestLocationUpdates(provider, minTimeMs, minDistance, listener)
awaitClose {
removeUpdates(listener)
}
}
You can use first() on the returned flow if you just want one update, and this will automatically support cancellation:
suspend fun LocationManager.listenLocationUpdate(): Location? =
withTimeoutOrNull(TIMEOUT) {
locationManager.locationUpdates("gps", 1000).first()
}
If you use numUpdates = 1 in your location request, you should also be able to wrap the listener-based API into a single-shot suspending function too. Cancellation here could be done by just removing the listener.

What is the appropriate way of calling suspending functions inside a suspendCoroutine block?

I need to call a suspending function inside a suspendCoroutine block, before I call continuation.resume().
What is the appropriate way of doing that?
private suspend fun someFunction() = suspendCoroutine { cont ->
//...
val myResult = mySuspendingFunction() //<--- The IDE says "Suspension functions can be called only within coroutine body"
cont.resume(myResult)
}
You can't call a suspend function in suspendCoroutine block, because it accepts non suspend block as parameter:
suspend inline fun <T> suspendCoroutine(
crossinline block: (Continuation<T>) -> Unit
): T
'suspendCoroutine' mainly used when we have some legacy code with callbacks, e.g.:
suspend fun getUser(id: String): User = suspendCoroutine { continuation ->
Api.getUser(id) { user ->
continuation.resume(user)
}
}
If function someFunction() doesn't call Api with callbacks then you should reconsider your approach getting rid of 'suspendCoroutine':
private suspend fun someFunction() {
// ...
val myResult = mySuspendingFunction()
// ...
}
If you still want to use suspendCoroutine move call of mySuspendingFunction out of suspendCoroutine block:
private suspend fun someFunction(): String {
val myResult = mySuspendingFunction()
return suspendCoroutine { cont ->
//...
cont.resume(myResult)
}
}
suspend fun mySuspendingFunction(): String {
delay(1000) // simulate request
return "result"
}
It's best to avoid this and call the suspending function before suspendCoroutine, as others have answered. That is possible for the specific case in question.
However, that is not possible if you need the continuation.
(The following is for those, who found this question for the this reason, as #Zordid and I have. chan.send is an example of this.)
In which case, the following is a possible, but error prone way to do it, that I do not recommend:
suspend fun cont1() {
//btw. for correct implementation, this should most likely be at least suspendCancellableCoroutine
suspendCoroutine<Unit> { uCont ->
val x = suspend { chan.send(foo(uCont)) }
x.startCoroutine(Continuation(uCont.context) {
if (it.isFailure)
uCont.resumeWith(it)
// else resumed by whatever reads from chan
})
}
}
(I think the error handling alone illustrates why it's not a great option, despite other problems.)
A better, safer and cheaper way is to use CompletableDeferred if you can.
If you must pass in a Continuation, it's still safer and probably cheaper to do:
suspend fun cont2() {
val rslt = CompletableDeferred<Unit>()
chan.send(foo(Continuation(currentCoroutineContext()) {
rslt.completeWith(it)
}))
rslt.await()
}

How do you call a suspend function inside a SAM?

I'm trying to create a Flow that needs to emit values from a callback but I can't call the emit function since the SAM is a normal function
Here's the class with the SAM from a library that I can't really modify it the way I need it to be.
class ValueClass {
fun registerListener(listener: Listener) {
...
}
interface Listener {
fun onNewValue(): String
}
}
And here's my take on creating the Flow object
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
emit(value) // Suspension functions can only be called on coroutine body
}
}
}
I guess it would've been simple if I could change the ValueClass but in this case, I can't. I've been wrapping my head around this and trying to look for implementations.
At least from what I know so far, one solution would be to use GlobalScope like this
class MyClass(private val valueClass: ValueClass) {
fun listenToValue = flow<String> {
valueClass.registerListener { value ->
GlobalScope.launch {
emit(value)
}
}
}
}
Now, this works but I don't want to use GlobalScope since I'll be using viewModelScope to tie it to my app's lifecycle.
Is there any way to work around this?
Thanks in advance. Any help would be greatly appreciated!
You can use callbackFlow to create a Flow from the callback. It will look something like:
fun listenToValue(): Flow<String> = callbackFlow {
valueClass.registerListener { value ->
trySend(value)
channel.close() // close channel if no more values are expected
}
awaitClose { /*unregister listener*/ }
}
Or if only one value is expected from the callback, you can use suspendCoroutine or suspendCancellableCoroutine. It this case listenToValue() function must be suspend and later called from a coroutine(e.g. someScope.launch):
suspend fun listenToValue(): String = suspendCoroutine { continuation ->
valueClass.registerListener { value ->
continuation.resumeWith(value)
}
}

Use property as accessor for Kotlin Coroutine

Kotlin Coroutines question... struggling w/ using a property instead of a function being the accessor for an asynchronous call.
Background is that I am trying to use the FusedLocationProviderClient with the kotlinx-coroutines-play-services library in order to use the .await() method on the Task instead of adding callbacks...
Currently having a property getter kick out to a suspend function, but not sure on how to launch the coroutine properly in order to avoid the
required Unit found XYZ
error...
val lastUserLatLng: LatLng?
get() {
val location = lastUserLocation
return if (location != null) {
LatLng(location.latitude, location.longitude)
} else {
null
}
}
val lastUserLocation: Location?
get() {
GlobalScope.launch {
return#launch getLastUserLocationAsync() <--- ERROR HERE
}
}
private suspend fun getLastUserLocationAsync() : Location? = withContext(Dispatchers.Main) {
return#withContext if (enabled) fusedLocationClient.lastLocation.await() else null
}
Any thoughts on how to handle this?
Properties can't be asynchronous. In general you should not synchronize asynchronous calls. You'd have to return a Deferred and call await() on it when you need a value.
val lastUserLatLng: Deferredd<LatLng?>
get() = GlobalScope.async {
lastUserLocation.await()?.run {
LatLng(latitude, longitude)
}
}
val lastUserLocation: Deferred<Location?>
get() = GlobalScope.async {
getLastUserLocationAsync()
}
private suspend fun getLastUserLocationAsync() : Location? = withContext(Dispatchers.Main) {
return#withContext if (enabled) fusedLocationClient.lastLocation.await() else null
}
But technically it's possible, though you should not do it. runBlocking() blocks until a value is available and returns it.

Categories

Resources