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.

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.

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.

Note

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.

Code Changes

CHANGED: /app/src/main/java/com/androidbyexample/googlemap/MainActivity.kt
package com.androidbyexample.googlemap

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibilityimport androidx.compose.animation.EnterTransitionimport androidx.compose.animation.fadeOutimport androidx.compose.foundation.backgroundimport androidx.compose.foundation.layout.Boximport androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.wrapContentSizeimport androidx.compose.material3.CircularProgressIndicatorimport androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Modifier
import com.androidbyexample.googlemap.ui.theme.GoogleMapTheme
import com.google.android.gms.maps.model.CameraPosition
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.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState

private val googleHQ = LatLng(37.42423291057923, -122.08811454627153)
private val defaultCameraPosition = CameraPosition.fromLatLngZoom(googleHQ, 11f)
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { GoogleMapTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) {
val cameraPositionState = rememberCameraPositionState { position = defaultCameraPosition }
GoogleMapDisplay( place = googleHQ, placeDescription = "Google HQ", cameraPositionState = cameraPositionState, modifier = Modifier.fillMaxSize(), )
} } } } }
@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() ) } }
}