Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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))
}
}
@LouisCAD

This comment has been minimized.

Copy link
Owner Author

@LouisCAD LouisCAD commented Sep 9, 2019

Fixed an NPE: last location can be null.

@sp00ne

This comment has been minimized.

Copy link

@sp00ne sp00ne commented Apr 29, 2020

Hm, I have done something very similar and it seems I can't get the awaitClose to fire when launched with lifecycleScope. Did you verify it?

EDIT: Sorry my bad, I didn't use viewLifecycleOwner.lifecycleScope. A quick addition as well. Multiple calls to this util would create multiple location requests correct? It would seem a bit heavy. Having it exposed as a property, wrapping this in a singleton-injectable-class and sharing the property would be more ideal no? I know that the Flow.share() is not released yet but what are your thoughts ? =)

@LouisCAD

This comment has been minimized.

Copy link
Owner Author

@LouisCAD LouisCAD commented Apr 29, 2020

@sp00ne I'm using toLiveData from AndroidX Lifecycle LiveData KTX (which you can convert back to flow after keeping the same reference for the middle sharing livedata). The timeout is 5 seconds by default.

@sp00ne

This comment has been minimized.

Copy link

@sp00ne sp00ne commented May 5, 2020

@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:

fun <T> LiveData<T>.asFlow(): Flow<T> = flow {
    val channel = Channel<T>(Channel.CONFLATED)
    val observer = Observer<T> {
        channel.offer(it)
    }
    withContext(Dispatchers.Main.immediate) {
        observeForever(observer)
    }
    try {
        for (value in channel) {
            emit(value)
        }
    } finally {
        GlobalScope.launch(Dispatchers.Main.immediate) {
            removeObserver(observer)
        }
    }
}

Or am I missing something? I.e if you do this:

liveDataThing.asFlow()
         .catch {
               Log.d("Error caught")
         }
         .launchIn(someScope)

and LiveData receives an error -> "catch" won't be reached, rather the scope will receive the error.

@LouisCAD

This comment has been minimized.

Copy link
Owner Author

@LouisCAD LouisCAD commented May 6, 2020

@sp00ne You need to handle the errors before converting to a LiveData.

@sp00ne

This comment has been minimized.

Copy link

@sp00ne sp00ne commented May 7, 2020

@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 🤷

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.