Skip to content

Instantly share code, notes, and snippets.

@LouisCAD
Last active January 31, 2024 20:56
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save LouisCAD/0a648e2b49942acd2acbb693adfaa03a to your computer and use it in GitHub Desktop.
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.
/*
* 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))
}
}
@stri8ed
Copy link

stri8ed commented Dec 29, 2022

locationClient.requestLocationUpdates(locationRequest, locationCallback, null).await()

The await call seems redundant, since the awaitClose block will keep the channel open.

@neronguyenvn
Copy link

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