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() ) } }
}
}