Blog Infos
Author
Published
Topics
,
Published

Jetpack Compose is gaining popularity and I personally think it is the future of Android or even multiplatform development. The library is now stable (version 1.0.1 now), however, there are still some of the view components that are not fully compatible with Compose. For those views, we can use @Composable AndroidView component and manage the updates in a composable. This blog post will share how I made an Image with a google map snapshot that updates properly when the state changes.

The Google maps API provide a callback to prepare the snapshot bitmap from the currently visible map. To apply this, we need a view with Google Maps. To make it work with the Compose, we need to initialize the map in AndroidView. You can check how to use Map in Compose, investigating Crane app code from official google samples.

@Composable
fun GoogleMapSnapshot(location: LatLng) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
val mapView = rememberMapViewWithLifecycle()
MapViewContainer(
map = mapView,
location = location
)
}
}
@Composable
private fun MapViewContainer(
map: MapView,
location: LatLng
) {
val coroutineScope = rememberCoroutineScope()
AndroidView({ map }) { mapView ->
coroutineScope.launch {
val googleMap = mapView.awaitMap()
val zoom = calculateZoom(cameraPosition)
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(location, zoom))
googleMap.addMarker { position(cameraPosition) }
}
}
}
                   Interactive map inside Composable AndroidView

The above code will display an interactive map with a screen width and height of 200dp. Now we need to take a snapshot and put it into Image. GoogleMap has a public snapshot lambda function that returns android.graphics.Bitmap. It uses SnapshotReadyCallback.

Since we are modifying the googleMap object in the update callback of @Composable AndroidView() , we cannot create an Image there(Composable invocations can only happen from the context of a Composable function). Therefore Image will be put in parent composable function, Box. In this scenario, the bitmap will be assigned to the function field as follows:

val mapBitmap: MutableState<Bitmap?> = remember { mutableStateOf(null) }

The @Composable Image cannot take bitmap as a parameter, but ImageBitmap. Luckily there is a converter function in compose graphics package.

Image(
    bitmap = mapBitmap.value.asImageBitmap(),
    contentDescription = "Map snapshot"
)

Connecting all the blocks:

@Composable
fun MapSnapshot(map: MapView, location: LatLng) {
val mapBitmap: MutableState<Bitmap?> = remember { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
if (mapBitmap.value != null) {
Image(
bitmap = mapBitmap.value!!.asImageBitmap(),
contentDescription = "Map snapshot",
)
} else {
AndroidView({ map }) { mapView ->
coroutineScope.launch {
val googleMap = mapView.awaitMap()
val zoom = calculateZoom()
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(location, zoom))
googleMap.addMarker { position(location) }
googleMap.snapshot {
mapBitmap.value = it
}
}
}
}
}
view raw MapSnaphot1.kt hosted with ❤ by GitHub

This code works nicely most of the time, but few things may cause some trouble. Firstly, the map sometimes is not rendered yet to take a snapshot. This really surprised me, cause we are configuring our map view in coroutine scope, calling moveCameraaddMarker and snapshot {} and yet map can be displayed blank:

This occurs because only awaitMap is suspend function. The rest of our config is simple java void methods, so we cannot be sure that the map is already positioned and rendered. We can fix that, adding another callback to the block, which is onMapLoaded. Putting the snapshot lambda inside this callback will result in a proper map view rendered into our snapshot image.

googleMap.setOnMapLoadedCallback {
    googleMap.snapshot {
        mapBitmap.value = it
    }
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Jetpack Compose: Drawing without pain and recomposition

This is a talk on recomposition in Jetpack Compose and the myths of too many calls it is followed by. I’ll briefly explain the reasons behind recompositions and why they are not as problematic as…
Watch Video

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jobs

                      Composable AndroidView presenting map

The second thing I encountered is the snapshot refreshing. Since the bitmap is held in remember{} , the AndroidView, even if it would be updated will not change the Image. We need to trigger the update on variables change. To handle such side effects, the compose API provides a few functions for extra effects. Here is the official doc.

Since the map needs a cleanup, the best choice here would be DisposableEffect. This effect runs every time a key parameter changes or if the DisposableEffect leaves the composition.

DisposableEffect(location) {
    mapBitmap.value = null
    onDispose { mapBitmap.value = null }
}

Now, whenever the location changes, the mapBitmap is set to null and the Composable function recompose with new values. The AndroidView with map renders a new map and provides a new bitmap for Image.

@Composable
fun MapSnapshot(map: MapView, location: LatLng) {
val mapBitmap: MutableState<Bitmap?> = remember { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
if (mapBitmap.value != null) {
Image(
bitmap = mapBitmap.value!!.asImageBitmap(),
contentDescription = "Map snapshot",
)
} else {
AndroidView({ map }) { mapView ->
coroutineScope.launch {
val googleMap = mapView.awaitMap()
val zoom = calculateZoom()
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(location, zoom))
googleMap.addMarker { position(location) }
googleMap.snapshot {
mapBitmap.value = it
}
}
}
}
DisposableEffect(location) {
mapBitmap.value = null
onDispose { mapBitmap.value = null }
}
}
view raw MapSnaphot2kt hosted with ❤ by GitHub
                          The final Composable MapSnapshot

In my opinion, the current support for views like Google Map is not ideal, but it works. The need for hacking is low. My only concern is that every time we create a bitmap we have to display the map. It would be great if we could prepare this image on the background thread and display the bitmap when it’s ready. If you know how to do that, write me on Twitter or in response to this post. I will be happy to chat and learn.

Thanks, Damian Petla for the early review. 😉

Happy composing 💻

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu