Travis Griggs
07/31/2023, 7:05 PMZoomAllButton(
action = {
var update =
CameraUpdateFactory.newLatLngBounds(sortedRegions.map { region -> region.coordinateBounds }
.reduce { merged, box -> merged.union(box) }, 20)
scope.launch { cameraState.animate(update, 300) }
}, modifier = Modifier.size(48.dp)
)
Passing these closures around, sometimes needing scope's to launch them, sometimes not, is still a bit unfamiliar for me. In this particular case, I find that I want to extract/refactor the action functionality. When my top level composable opens the first time, I want essentially "press" this button (well, zoom all) the same as if someone had pressed it.
I think the recipe for doing something just once on first compose is:
LaunchedEffect(true) { .. }
So essentially, rather than copy/paste the body there, I'm trying to figure out how to extract it and reuse it. It's unclear whether this should be another @Composable, when it seems more of a side effect. Should refactor Function... or Function to Scope... (I don't really understand the later refactoring). I feel like I'm just throwing hammers here to understand the execution/side effect model.Stylianos Gakis
07/31/2023, 7:34 PMvar
in there and not a val?Travis Griggs
07/31/2023, 7:40 PMfun zoomAll(cameraState: CameraPositionState, regions: List<MappedRegion>, scope: CoroutineScope) {
val bounds = regions.map { region -> region.coordinateBounds }
.reduce { merged, box -> merged.union(box) }
val update = CameraUpdateFactory.newLatLngBounds(bounds, 20)
scope.launch { cameraState.animate(update, 300) }
}
(I could possibly make the merged regions a derivedState, since sortedRegions is a mutable state -- do derived states recompute on demand or immeidately on any source change?) ... that I had made it a val
And now I can use it in two places:
LaunchedEffect(true) {
delay(100) // TODO: why is this necessary? don't like delays-to-make-things-work
zoomAll(cameraState, sortedRegions, this)
}
and
ZoomAllButton(
action = { zoomAll(cameraState, sortedRegions, scope) },
modifier = Modifier.size(48.dp)
)
Passing all 3 parameters seems... i dunno, but maybe that's just more normal in a more function oriented worldStylianos Gakis
07/31/2023, 7:42 PMTravis Griggs
07/31/2023, 7:49 PMfun CameraPositionState.zoomAll(...)
.
(there's a spin up here to doing a similar thing for my "pan to current location" functionality)Stylianos Gakis
07/31/2023, 7:55 PMTravis Griggs
07/31/2023, 11:11 PMsuspend fun CameraPositionState.zoomBounds(bounds: LatLngBounds) {
val update = CameraUpdateFactory.newLatLngBounds(bounds, 20)
animate(update, 300)
}
(I moved the merge computation outside, because there's an empty regions case I want to test against before calling this using reduceOrNull, rather than just reduce).
Where it got harder is my centerToCurrentLocation. It's working, but it feels pretty ugly:
suspend fun CameraPositionState.centerToCurrentLocation(
context: Context, isInitial: Boolean = true
) {
val locationClient = LocationServices.getFusedLocationProviderClient(context)
// TODO: Move this permission checking stuff somewhere else
if (ActivityCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
context, Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
// T Consider calling
// ActivityCompat#requestPermissions
// here to request the missing permissions, and then overriding
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
// int[] grantResults)
// to handle the case where the user grants the permission. See the documentation
// for ActivityCompat#requestPermissions for more details.
"!!!PERMISSIONS FAILURE!!!".logged()
return
}
val scope = CoroutineScope(coroutineContext)
scope.launch(<http://Dispatchers.IO|Dispatchers.IO>) {
await(
locationClient.getCurrentLocation(
PRIORITY_HIGH_ACCURACY, null
)
)?.let { location ->
val coord = LatLng(location.latitude, location.longitude)
val update = isInitial.opt({ CameraUpdateFactory.newLatLngZoom(coord, 15f) },
{ CameraUpdateFactory.newLatLng(coord) })
scope.launch { animate(update, 300) }
}
}
}
Part of the reason it's ugly, because of the need (or at least IDE's desire) to have me do the permissions check there, so I have to pass in a context. There's got to be a different/better way to do all of that, but I haven't squared that away yet. I know Accompanist had/has some permissions stuff, but it didn't seem very robust yet. I would love to see a strong/current example of how one does the permissions dance in a pure compose implementation (no view models, acitivities, fragments, etc).
That aside, the bottom part was still tricky (to me as well), because the location fetch has to happen off of the main thread (I guess), but the animate has to be back on the original scope/main thread. There's probably a way better to structure that (an extension on LocationClient maybe?), but the goal was to get it working. I'll try to do some more ingratiating with the ins and outs of coroutinism 😄
Thanks for your help guys.