Pairing with Aagya¶
Dhruva does not ask for permission. The cleanest pairing is with Aagya, the sister library for permissions.
The complete flow¶
@Composable
fun LocationFlow() {
val permissions = rememberPermissionController()
val tracker = rememberLocationTracker()
val scope = rememberCoroutineScope()
var lastFix by remember { mutableStateOf<Location?>(null) }
var statusLine by remember { mutableStateOf("Tap to find me") }
Button(onClick = {
scope.launch {
// 1. Make sure we have permission first.
when (val r = permissions.requestPermission(AppPermission.Location.Fine)) {
is PermissionResult.Granted -> Unit
is PermissionResult.Denied -> {
statusLine = if (r.canAskAgain) "Tap again to retry" else "Open Settings"
if (!r.canAskAgain) permissions.openAppSettings()
return@launch
}
is PermissionResult.Cancelled,
is PermissionResult.PolicyExhausted -> {
statusLine = "Permission required"
return@launch
}
}
// 2. Read a fix.
statusLine = "Locating..."
lastFix = runCatching { tracker.getCurrentLocation() }.getOrNull()
statusLine = lastFix?.let { "${it.latitude}, ${it.longitude}" }
?: "Couldn't determine location"
}
}) {
Text(statusLine)
}
}
Why two libraries¶
The two concerns are independent. Some apps need permission flows for a feature that doesn't involve location at all (camera, microphone, notifications). Some apps want location but already have a permission flow they're happy with. Bundling them would force you to take the dependency you don't need.
Both libraries follow the same conventions (sealed result types, never throw across the bridge, Compose-native factories) so they compose cleanly.
Streaming with permission gating¶
LaunchedEffect(Unit) {
val granted = permissions
.requestPermission(AppPermission.Location.Fine) is PermissionResult.Granted
if (!granted) return@LaunchedEffect
tracker.startTracking().collect { fix ->
map.center(fix)
}
}
If the user revokes permission while the flow is active, the next emission throws LocationError.PermissionDenied, which terminates the collector. Wrap in runCatching to handle that case gracefully.
Re-asking after the user revokes¶
When PermissionResult.Denied(canAskAgain = false) lands, the only path forward is permissions.openAppSettings(). After the user toggles the permission and returns to the app, your LaunchedEffect will re-run if its key changes, so a key1 = lifecycle or a manual permissionVersion integer that increments on resume can re-trigger the flow.