Google Map
Loading Spinner
Google map takes a little while to initialize. While it's loading, the user sees a blank screen, which may make them think the application isn't working.
Adding an animated progress indicator lets the user know the application isn't just hanging. We'll add an "indeterminate" (no obvious beginning/end/length) spinning indicator on top of the map for this.
First, let's wrap the GoogleMap
so we can contain it and the progress spinner that we'll create.
We do this using a Box
.
We need state to keep track of whether the map has finished loading. A simple remembered
MutableState
will do the trick.
This drives the display of the CircularProgressIndicator
show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...
@Composable
fun GoogleMapDisplay(
// ...
modifier: Modifier,
) {
var mapLoaded by remember { mutableStateOf(false) }
val placeState =
rememberMarkerState(key = place.toString(), position = place)
Box(
modifier = modifier,
) {
GoogleMap(
cameraPositionState = cameraPositionState,
onMapLoaded = {
mapLoaded = true
},
// modifier = modifier,
modifier = Modifier.fillMaxSize(),
) {
MarkerInfoWindowContent(
// ...
}
if (!mapLoaded) {
AnimatedVisibility(
visible = true,
modifier = Modifier.fillMaxSize(),
enter = EnterTransition.None,
exit = fadeOut()
) {
CircularProgressIndicator(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.wrapContentSize()
)
}
}
}
}
Note
Displaying or not displaying the progress indicator just takes an if
expression. Note that
we could have passed !mapLoaded
for the visible
parameter to AnimatedVisibility
, but
that means we'd still be looking at that Composable on every recomposition. Instead I chose to
add the if
expression around it.
This likely doesn't make a big difference here, but I wanted to demonstrate how normal Kotlin
logic can be used to conditionally display overlays like this. Some overlays that you use (like
a dialog) won't have a parameter like visible
.
AnimatedVisibility
is used here to fade-out the progress spinner when it's time to leave the
composition. Because it's after the GoogleMap
in the Box
, it appears on top of it. The solid
background prevents any part of the map from being visible before it's completely loaded.
When the map tells us it's loaded, we set mapLoaded
. When its value changes, recomposition is
triggered and changes whether or not we include the progress indicator in the composition tree.
show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...
@Composable
fun GoogleMapDisplay(
// ...
) {
// ...
Box(
// ...
) {
GoogleMap(
cameraPositionState = cameraPositionState,
onMapLoaded = {
mapLoaded = true
},
// modifier = modifier,
modifier = Modifier.fillMaxSize(),
) {
// ...
}
// ...
}
}
Did you notice how we changed where the passed-in Modifier
is used? We moved it from the
GoogleMap
to the to the Box
. This function declares the parent of the GoogleMap
(the Box
),
so we have full control over its Modifiers
.
show in full file app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
// ...
@Composable
fun GoogleMapDisplay(
// ...
) {
// ...
Box(
modifier = modifier,
) {
GoogleMap(
// ...
mapLoaded = true
},
// modifier = modifier,
modifier = Modifier.fillMaxSize(),
) {
MarkerInfoWindowContent(
// ...
}
// ...
}
}
All code changes
CHANGED: app/src/main/java/com/androidbyexample/compose/google/google/maps/GoogleMapDisplay.kt
package com.androidbyexample.compose.google.google.maps
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MarkerInfoWindowContent
import com.google.maps.android.compose.rememberMarkerState
@Composable
fun GoogleMapDisplay(
place: LatLng,
placeDescription: String,
cameraPositionState: CameraPositionState,
modifier: Modifier,
) {
var mapLoaded by remember { mutableStateOf(false) }
val placeState =
rememberMarkerState(key = place.toString(), position = place)
Box(
modifier = modifier,
) {
GoogleMap(
cameraPositionState = cameraPositionState,
onMapLoaded = {
mapLoaded = true
},
// modifier = modifier,
modifier = Modifier.fillMaxSize(),
) {
MarkerInfoWindowContent(
state = placeState,
title = placeDescription,
onClick = {
placeState.showInfoWindow()
true
}
)
}
if (!mapLoaded) {
AnimatedVisibility(
visible = true,
modifier = Modifier.fillMaxSize(),
enter = EnterTransition.None,
exit = fadeOut()
) {
CircularProgressIndicator(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.wrapContentSize()
)
}
}
}
}