Last active
May 23, 2024 12:11
-
-
Save LouisCAD/0a648e2b49942acd2acbb693adfaa03a to your computer and use it in GitHub Desktop.
Create a Flow of location updates on Android (using kotlinx.coroutines), backed by Fused Location Provider from Google Play Services.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright 2019 Louis Cognault Ayeva Derman. Use of this source code is governed by the Apache 2.0 license. | |
*/ | |
import android.location.Location | |
import com.google.android.gms.location.LocationCallback | |
import com.google.android.gms.location.LocationRequest | |
import com.google.android.gms.location.LocationResult | |
import com.google.android.gms.location.LocationServices | |
import kotlinx.coroutines.CancellationException | |
import kotlinx.coroutines.ExperimentalCoroutinesApi | |
import kotlinx.coroutines.channels.SendChannel | |
import kotlinx.coroutines.channels.awaitClose | |
import kotlinx.coroutines.flow.Flow | |
import kotlinx.coroutines.flow.channelFlow | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.tasks.await | |
import splitties.init.appCtx // Similar to Firebase auto-initialization of applicationContext, | |
// usages can be replaced with Context receiver or parameter if you really enjoy parameter passing. | |
@ExperimentalCoroutinesApi | |
@Throws(SecurityException::class) | |
inline fun fusedLocationFlow( | |
configLocationRequest: LocationRequest.() -> Unit | |
): Flow<Location> = fusedLocationFlow( | |
locationRequest = LocationRequest.create().apply(configLocationRequest) | |
) | |
@Throws(SecurityException::class) | |
@ExperimentalCoroutinesApi | |
fun fusedLocationFlow( | |
locationRequest: LocationRequest | |
): Flow<Location> = channelFlow { | |
val locationClient = LocationServices.getFusedLocationProviderClient(appCtx) | |
val locationCallback = object : LocationCallback() { | |
override fun onLocationResult(result: LocationResult) { | |
result.locations.forEachByIndex { offerCatching(it) } | |
} | |
} | |
locationClient.lastLocation.await<Location?>()?.let { send(it) } | |
locationClient.requestLocationUpdates(locationRequest, locationCallback, null).await() | |
awaitClose { | |
locationClient.removeLocationUpdates(locationCallback) | |
} | |
} | |
/** | |
* [SendChannel.offer] that returns `false` when this [SendChannel.isClosedForSend], instead of | |
* throwing. | |
* | |
* [SendChannel.offer] throws when the channel is closed. In race conditions, especially when using | |
* multithreaded dispatchers, that can lead to uncaught exceptions as offer is often called from | |
* non suspending functions that don't catch the default [CancellationException] or any other | |
* exception that might be the cause of the closing of the channel. | |
*/ | |
// Copy pasted from splitties.coroutines | |
fun <E> SendChannel<E>.offerCatching(element: E): Boolean { | |
return runCatching { offer(element) }.getOrDefault(false) | |
} | |
/** | |
* Iterates the receiver [List] using an index instead of an [Iterator] like [forEach] would do. | |
* Using this function saves an [Iterator] allocation, which is good for immutable lists or usages | |
* confined to a single thread like UI thread only use. | |
* However, this method will not detect concurrent modification, except if the size of the list | |
* changes on an iteration as a result, which may lead to unpredictable behavior. | |
* | |
* @param action the action to invoke on each list element. | |
*/ | |
// Copy pasted from splitties.collections | |
inline fun <T> List<T>.forEachByIndex(action: (T) -> Unit) { | |
val initialSize = size | |
for (i in 0..lastIndex) { | |
if (size != initialSize) throw ConcurrentModificationException() | |
action(get(i)) | |
} | |
} |
@sp00ne You need to handle the errors before converting to a LiveData
.
@LouisCAD i.e "catch" is not going to fire downstream making it sort of invalid for attempts of retrying or handling it in a "Flow"-manner 🤷
I wish you could update it. Appreciate so much. Thank you.
locationClient.requestLocationUpdates(locationRequest, locationCallback, null).await()
The await
call seems redundant, since the awaitClose
block will keep the channel open.
i think so, i already made this class today and it worked without await()
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@LouisCAD Won't making it a LiveData inbetween cause exceptions not to be handled? If you look at the source for
.asFlow()
it doesn't pass the exception:Or am I missing something? I.e if you do this:
and LiveData receives an error -> "catch" won't be reached, rather the scope will receive the error.