Home > Software design >  Android - callbackFlow in Singleton class
Android - callbackFlow in Singleton class

Time:10-04

When I have my Location Provider as a Singleton, and assume I don't have the location permission and my condition closes the flow.

Then calling highAccuracyLocationFlow() function won't create callbackFlow again.

If I remove the @singleton, it works but creates multiple instances of the class for each subscriber.

How can I go about it?

@Singleton
class DefaultLocationProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val fusedLocationClient: FusedLocationProviderClient,
) : LocationProvider {

init {
    Timber.d("init: ")
}

private val _receivingLocationUpdates: MutableStateFlow<Boolean> =
    MutableStateFlow(false)

override val receivingLocationUpdates: StateFlow<Boolean>
    get() = _receivingLocationUpdates

private var _lastKnownLocation : Location? = null

override fun getLastKnownLocation(): Location? {
    return _lastKnownLocation
}

private lateinit var locationRequest: LocationRequest

private val highAccuracyLocationRequest = LocationRequest.create().apply {
    interval = TimeUnit.SECONDS.toMillis(2)
    fastestInterval = TimeUnit.SECONDS.toMillis(1)
    priority = Priority.PRIORITY_HIGH_ACCURACY
    smallestDisplacement = 0f
}

private val balancedPowerLocationRequest = LocationRequest.create().apply {
    interval = TimeUnit.SECONDS.toMillis(60)
    fastestInterval = TimeUnit.SECONDS.toMillis(30)
    priority = Priority.PRIORITY_BALANCED_POWER_ACCURACY
    smallestDisplacement = 50f
}

@SuppressLint("MissingPermission")
private val _locationUpdates = callbackFlow {
    val callback = object : LocationCallback() {
        override fun onLocationResult(result: LocationResult) {
            Timber.d("New location: ${result.lastLocation.toString()}")
            // Send the new location to the Flow observers
            _lastKnownLocation = result.lastLocation
            result.lastLocation?.let {
                trySend(it).isSuccess
            }
        }
    }

    if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
        != PackageManager.PERMISSION_GRANTED ||
        ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
        != PackageManager.PERMISSION_GRANTED
    ) close()

    Timber.d("Starting location updates")
    _receivingLocationUpdates.value = true

    fusedLocationClient.requestLocationUpdates(
        locationRequest,
        callback,
        Looper.getMainLooper()
    ).addOnFailureListener { e ->
        e.printStackTrace()
        close(e) // in case of exception, close the Flow
    }

    awaitClose {
        Timber.d("Stopping location updates")
        _receivingLocationUpdates.value = false
        fusedLocationClient.removeLocationUpdates(callback) // clean up when Flow collection ends
    }
}.shareIn(
    MainScope(),
    replay = 1,
    started = SharingStarted.WhileSubscribed()
)

override fun highAccuracyLocationFlow(): Flow<Location> {
    Timber.d("highAccuracyLocationFlow req")
    locationRequest = highAccuracyLocationRequest
    return _locationUpdates
}

override fun balancedPowerLocationFlow(): Flow<Location> {
    locationRequest = balancedPowerLocationRequest
    return _locationUpdates
}

}

CodePudding user response:

Side note, I see a bug in your code. You used _locationUpdates with shareIn so there can only be one session of requestLocationUpdates going on at a time. So if you call balancedPowerLocationFlow() and start collecting that flow, followed by calling highAccuracyLocationFlow() while there is still a subscriber of the balanced power flow, it will remain as a balanced power flow even for the new subscriber.


Here are a couple of different strategies for the issue you're asking about:

  1. Mark your two functions with @RequiresPermission(ACCESS_FINE_LOCATION) so you are leaving it up to the caller to only get the Flow reference if it knows the permission is already granted. The annotation helps catch some situations where you accidentally forget to check for the permission first. If you do this, you can remove the safety check that closes the Flow.

  2. Create an internal Channel for tracking when the permission has been granted. Any class that uses this class can be responsible for informing it when permission has been granted (or that permission has already been granted).

private val permissionGrantedChannel = Channel<Unit>()

fun notifyLocationPermissionGranted() {
    permissionGrantedChannel.trySend(Unit)
}

Then you can replace your if(/*...*/) close() with permissionGrantedChannel.receive() so the Flow simply suspends until it is known that the permissions have been granted.

Edit: Actually, this should probably be a MutableStateFlow instead of channel so when your flow gets restarted due to falling to 0 subscribers momentarily, the true value is already there. I put this version in the code below.


Here is a potential strategy for the issue I mentioned at the top. I didn't test this. The idea here is that we keep track in a StateFlow of how many high-accuracy subscriptions are currently being collected and use flatMapLatest on that to automatically restart our flow with the right type of location updates whenever we move between 0 and 1 collectors that require high accuracy.

A high-accuracy flow is wrapped at the start and end with making updates to that StateFlow, but otherwise is just passing through the same flow as you would get if requesting balanced updates. So there is only ever one location request going on at once. Collectors that only want balanced updates will simply get temporarily swapped to high accuracy whenever there is at least one high accuracy collector simultaneously getting updates.

Note, this is just to illustrate the concept. I removed a lot of your boilerplate just for brevity.

private val scope = MainScope()   CoroutineName("DefaultLocationProvider CoroutineScope")
private var highAccuracyCollectorCount = 0
private val isHighAccuracy = MutableStateFlow(false)
private val permissionGranted = MutableStateFlow(false)

fun notifyLocationPermissionGranted() {
    permissionGranted.value = true
}

@OptIn(ExperimentalCoroutinesApi::class)
val balancedPowerLocations = isHighAccuracy.flatMapLatest { shouldUseHighAccuracy ->
    callbackFlow {
        val callback = object : LocationCallback() {
            //...
        }
        permissionGranted.first { it }

        fusedLocationClient.requestLocationUpdates(
            locationRequest,
            callback,
            Looper.getMainLooper()
        ).addOnFailureListener { e ->
            e.printStackTrace()
            close(e) // in case of exception, close the Flow
        }
        awaitClose {
            fusedLocationClient.removeLocationUpdates(callback)
        }
    }
}
    .distinctUntilChanged() // in case switching callbacks tries to replay the last known location
    .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)

val highAccuracyLocations = balancedPowerLocations
    .onStart {
        isHighAccuracy.value = true
        highAccuracyCollectorCount  
    }
    .onCompletion {
        isHighAccuracy.value = --highAccuracyCollectorCount > 0
    }
  • Related