1. Before You Begin
This codelab teaches you how to integrate Maps SDK for Android with your app and use its core features by building an app that displays a map of mountains in Colorado, USA, using various types of markers. Additionally, you'll learn to draw other shapes on the map.
Here's what it will look like when you are finished with the codelab:
Prerequisites
- Basic knowledge of Kotlin, Jetpack Compose, and Android development
What you'll do
- Enable and use the Maps Compose library for the Maps SDK for Android to add a
GoogleMap
to an Android app - Add and customize markers
- Draw polygons on the map
- Control the viewpoint of the camera programmatically
What you'll need
- Maps SDK for Android
- A Google Account with billing enabled
- Latest stable version of Android Studio
- An Android device or an Android emulator that runs the Google APIs platform based on Android 5.0 or higher (see Run apps on the Android Emulator for installation steps.)
- An internet connection
2. Get set up
For the following enablement step, you need to enable Maps SDK for Android.
Set up Google Maps Platform
If you do not already have a Google Cloud Platform account and a project with billing enabled, please see the Getting Started with Google Maps Platform guide to create a billing account and a project.
- In the Cloud Console, click the project drop-down menu and select the project that you want to use for this codelab.
- Enable the Google Maps Platform APIs and SDKs required for this codelab in the Google Cloud Marketplace. To do so, follow the steps in this video or this documentation.
- Generate an API key in the Credentials page of Cloud Console. You can follow the steps in this video or this documentation. All requests to Google Maps Platform require an API key.
3. Quick start
To get you started as quickly as possible, here's some starter code to help you follow along with this codelab. You are welcome to jump to the solution, but if you want to follow along with all the steps to build it yourself, keep reading.
- Clone the repository if you have
git
installed.
git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Alternatively, you can click the following button to download the source code.
- Upon getting the code, go ahead and open the project found inside the
starter
directory in Android Studio.
4. Add your API key to the project
This section describes how to store your API key so that it can be securely referenced by your app. You shouldn't check your API key into your version control system, so we recommend storing it in the secrets.properties
file, which will be placed in your local copy of the root directory of your project. For more information about the secrets.properties
file, see Gradle properties files.
To streamline this task, we recommend that you use the Secrets Gradle Plugin for Android.
To install the Secrets Gradle Plugin for Android in your Google Maps project:
- In Android Studio, open your top-level
build.gradle.kts
file and add the following code to thedependencies
element underbuildscript
.buildscript { dependencies { classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") } }
- Open your module-level
build.gradle.kts
file and add the following code to theplugins
element.plugins { // ... id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") }
- In your module-level
build.gradle.kts
file, ensure thattargetSdk
andcompileSdk
are set to at least 34. - Save the file and sync your project with Gradle.
- Open the
secrets.properties
file in your top-level directory, and then add the following code. ReplaceYOUR_API_KEY
with your API key. Store your key in this file becausesecrets.properties
is excluded from being checked into a version control system.MAPS_API_KEY=YOUR_API_KEY
- Save the file.
- Create the
local.defaults.properties
file in your top-level directory, the same folder as thesecrets.properties
file, and then add the following code.
The purpose of this file is to provide a backup location for the API key if theMAPS_API_KEY=DEFAULT_API_KEY
secrets.properties
file is not found so that builds don't fail. This will happen when you clone the app from a version control system and you have not yet created asecrets.properties
file locally to provide your API key. - Save the file.
- In your
AndroidManifest.xml
file, go tocom.google.android.geo.API_KEY
and update theandroid:value
attribute. If the<meta-data>
tag doesn't exist, create it as a child of the<application>
tag.<meta-data android:name="com.google.android.geo.API_KEY" android:value="${MAPS_API_KEY}" />
- In Android Studio, open your module-level
build.gradle.kts
file and edit thesecrets
property. If thesecrets
property does not exist, add it.Edit the properties of the plugin to setpropertiesFileName
tosecrets.properties
, setdefaultPropertiesFileName
tolocal.defaults.properties
, and set any other properties.secrets { // Optionally specify a different file name containing your secrets. // The plugin defaults to "local.properties" propertiesFileName = "secrets.properties" // A properties file containing default secret values. This file can be // checked in version control. defaultPropertiesFileName = "local.defaults.properties" }
5. Add Google Maps
In this section, you will add a Google Map so that it loads when you launch the app.
Add Maps Compose dependencies
Now that your API key can be accessed inside the app, the next step is to add the Maps SDK for Android dependency to your app's build.gradle.kts
file. To build with Jetpack Compose, use the Maps Compose library that provides elements of the Maps SDK for Android as composable functions and data types.
build.gradle.kts
In the app level build.gradle.kts
file replace the non-compose Maps SDK for Android dependencies:
dependencies {
// ...
// Google Maps SDK -- these are here for the data model. Remove these dependencies and replace
// with the compose versions.
implementation("com.google.android.gms:play-services-maps:18.2.0")
// KTX for the Maps SDK for Android library
implementation("com.google.maps.android:maps-ktx:5.0.0")
// KTX for the Maps SDK for Android Utility Library
implementation("com.google.maps.android:maps-utils-ktx:5.0.0")
}
with their composable counterparts:
dependencies {
// ...
// Google Maps Compose library
val mapsComposeVersion = "4.4.1"
implementation("com.google.maps.android:maps-compose:$mapsComposeVersion")
// Google Maps Compose utility library
implementation("com.google.maps.android:maps-compose-utils:$mapsComposeVersion")
// Google Maps Compose widgets library
implementation("com.google.maps.android:maps-compose-widgets:$mapsComposeVersion")
}
Add a Google Map composable
In MountainMap.kt
, add the GoogleMap
composable inside the Box
composable nested within the MapMountain
composable.
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.GoogleMapComposable
// ...
@Composable
fun MountainMap(
paddingValues: PaddingValues,
viewState: MountainsScreenViewState.MountainList,
eventFlow: Flow<MountainsScreenEvent>,
selectedMarkerType: MarkerType,
) {
var isMapLoaded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
// Add GoogleMap here
GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapLoaded = { isMapLoaded = true }
)
// ...
}
}
Now build and run the app. Behold! You should see a map centered on the notorious Null Island, also known as latitude zero and longitude zero. Later, you'll learn how to position the map to the location and zoom level that you want, but for now celebrate your first victory!
6. Cloud-based map styling
You can customize the style of your map using Cloud-based map styling.
Create a Map ID
If you have not yet created a map ID with a map style associated to it, see the Map IDs guide to complete the following steps:
- Create a map ID.
- Associate a map ID to a map style.
Add the Map ID to your app
To use the map ID you created, when instantiating your GoogleMap
composable, use the map ID when creating a GoogleMapOptions
object which is assigned to the googleMapOptionsFactory
parameter in the constructor.
GoogleMap(
// ...
googleMapOptionsFactory = {
GoogleMapOptions().mapId("MyMapId")
}
)
Once you've completed this, go ahead and run the app to see your map in the style that you selected!
7. Load the marker data
The main task of the app is to load a collection of mountains from local storage and display those mountains in the GoogleMap
. In this step, you'll take a tour of the provided infrastructure for loading the mountain data and presenting it to the UI.
Mountain
The Mountain
data class holds all of the data about each mountain.
data class Mountain(
val id: Int,
val name: String,
val location: LatLng,
val elevation: Meters,
)
Note that the mountains will later be partitioned based on their elevation. Mountains that are at least 14,000 feet tall are called fourteeners. The starter code includes an extension function do this check for you.
/**
* Extension function to determine whether a mountain is a "14er", i.e., has an elevation greater
* than 14,000 feet (~4267 meters).
*/
fun Mountain.is14er() = elevation >= 14_000.feet
MountainsScreenViewState
The MountainsScreenViewState
class holds all of the data needed to render the view. It can either be in a Loading
or MountainList
state depending on whether the list of mountains has finished loading.
/**
* Sealed class representing the state of the mountain map view.
*/
sealed class MountainsScreenViewState {
data object Loading : MountainsScreenViewState()
data class MountainList(
// List of the mountains to display
val mountains: List<Mountain>,
// Bounding box that contains all of the mountains
val boundingBox: LatLngBounds,
// Switch indicating whether all the mountains or just the 14ers
val showingAllPeaks: Boolean = false,
) : MountainsScreenViewState()
}
Provided classes: MountainsRepository
and MountainsViewModel
In the starter project, the class MountainsRepository
has been provided for you. This class reads a list of mountains places that are stored in a GPS Exchange Format
, or GPX file, top_peaks.gpx
. Calling mountainsRepository.loadMountains()
returns a StateFlow<List<Mountain>>
.
MountainsRepository
class MountainsRepository(@ApplicationContext val context: Context) {
private val _mountains = MutableStateFlow(emptyList<Mountain>())
val mountains: StateFlow<List<Mountain>> = _mountains
private var loaded = false
/**
* Loads the list of mountains from the list of mountains from the raw resource.
*/
suspend fun loadMountains(): StateFlow<List<Mountain>> {
if (!loaded) {
loaded = true
_mountains.value = withContext(Dispatchers.IO) {
context.resources.openRawResource(R.raw.top_peaks).use { inputStream ->
readMountains(inputStream)
}
}
}
return mountains
}
/**
* Reads the [Waypoint]s from the given [inputStream] and returns a list of [Mountain]s.
*/
private fun readMountains(inputStream: InputStream) =
readWaypoints(inputStream).mapIndexed { index, waypoint ->
waypoint.toMountain(index)
}.toList()
// ...
}
MountainsViewModel
MountainsViewModel
is a ViewModel
class which loads the collections of mountains and exposes that collections as well as other parts of the UI state via the mountainsScreenViewState
. mountainsScreenViewState
is a hot StateFlow
that the UI can observe as a mutable state using the collectAsState
extension function.
Following sound architectural principles, MountainsViewModel
holds all the app's state. The UI sends user interactions to the view model using the onEvent
method.
@HiltViewModel
class MountainsViewModel
@Inject
constructor(
mountainsRepository: MountainsRepository
) : ViewModel() {
private val _eventChannel = Channel<MountainsScreenEvent>()
// Event channel to send events to the UI
internal fun getEventChannel() = _eventChannel.receiveAsFlow()
// Whether or not to show all of the high peaks
private var showAllMountains = MutableStateFlow(false)
val mountainsScreenViewState =
mountainsRepository.mountains.combine(showAllMountains) { allMountains, showAllMountains ->
if (allMountains.isEmpty()) {
MountainsScreenViewState.Loading
} else {
val filteredMountains =
if (showAllMountains) allMountains else allMountains.filter { it.is14er() }
val boundingBox = filteredMountains.map { it.location }.toLatLngBounds()
MountainsScreenViewState.MountainList(
mountains = filteredMountains,
boundingBox = boundingBox,
showingAllPeaks = showAllMountains,
)
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = MountainsScreenViewState.Loading
)
init {
// Load the full set of mountains
viewModelScope.launch {
mountainsRepository.loadMountains()
}
}
// Handle user events
fun onEvent(event: MountainsViewModelEvent) {
when (event) {
OnZoomAll -> onZoomAll()
OnToggleAllPeaks -> toggleAllPeaks()
}
}
private fun onZoomAll() {
sendScreenEvent(MountainsScreenEvent.OnZoomAll)
}
private fun toggleAllPeaks() {
showAllMountains.value = !showAllMountains.value
}
// Send events back to the UI via the event channel
private fun sendScreenEvent(event: MountainsScreenEvent) {
viewModelScope.launch { _eventChannel.send(event) }
}
}
If you are curious about the implementation of these classes, you can access them on GitHub or open the MountainsRepository
and MountainsViewModel
classes in Android Studio.
Use the ViewModel
The view model is used in MainActivity
to get the viewState
. You will use the viewState
to the render the markers later in this codelab. Note this code is already included in the starter project and is shown here for reference only.
val viewModel: MountainsViewModel by viewModels()
val screenViewState = viewModel.mountainsScreenViewState.collectAsState()
val viewState = screenViewState.value
8. Position the camera
A GoogleMap
default centers to latitude zero, longitude zero. The markers you will be rendering are located in State of Colorado in the USA. The viewState
provided by the view model presents a LatLngBounds which contains all of the markers.
In MountainMap.kt
create a CameraPositionState
initialized to the center of the bounding box. Set the cameraPositionState
parameter of the GoogleMap
to the cameraPositionState
variable you just created.
fun MountainMap(
// ...
) {
// ...
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(viewState.boundingBox.center, 5f)
}
GoogleMap(
// ...
cameraPositionState = cameraPositionState,
)
}
Now run the code and watch the map center on Colorado.
Zoom to the marker extents
To really focus the map on the markers add the zoomAll
function to the end of the MountainMap.kt
file. Note that this function needs a CoroutineScope
because animating the camera to a new location is an asynchronous operation that takes time to complete.
fun zoomAll(
scope: CoroutineScope,
cameraPositionState: CameraPositionState,
boundingBox: LatLngBounds
) {
scope.launch {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngBounds(boundingBox, 64),
durationMs = 1000
)
}
}
Next, add code to invoke the zoomAll
function whenever the bounds around the marker collection changes or when the user clicks the zoom extents button in the TopApp bar. Note the zoom extents button is already wired up to send events to the view model. You only need to collect those events from the view model and call the zoomAll
function in response.
fun MountainMap(
// ...
) {
// ...
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = viewState.boundingBox) {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
LaunchedEffect(true) {
eventFlow.collect { event ->
when (event) {
MountainsScreenEvent.OnZoomAll -> {
zoomAll(scope, cameraPositionState, viewState.boundingBox)
}
}
}
}
}
Now when you run the app, the map will start focused over the area where the markers will go. You can reposition and change the zoom and clicking the zoom extents button will refocus the map around the marker area. That's forward progress! But the map really should have something to look at. And that's what you'll do that in the next step!
9. Basic markers
In this step, you add Markers to the map that represent points of interest that you want to highlight on the map. You will use the list of mountains that have been provided in the starter project and add these places as markers on the map.
Start by adding a content block to the GoogleMap
. There will be multiple marker types, so add a when
statement to branch to each type and you'll implement each in turn in the proceeding steps.
GoogleMap(
// ...
) {
when (selectedMarkerType) {
MarkerType.Basic -> {
BasicMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Advanced -> {
AdvancedMarkersMapContent(
mountains = viewState.mountains,
)
}
MarkerType.Clustered -> {
ClusteringMarkersMapContent(
mountains = viewState.mountains,
)
}
}
}
Add markers
Annotate BasicMarkersMapContent
with @GoogleMapComposable
. Note that you are limited to using @GoogleMapComposable
functions in the GoogleMap
content block. The mountains
object has a list of Mountain
objects. You will add a marker for each mountain in that list, using the location, name, and elevation from the Mountain
object. The location is used to set the Marker
's state parameter which, in turn, controls the marker's position.
// ...
import com.google.android.gms.maps.model.Marker
import com.google.maps.android.compose.GoogleMapComposable
import com.google.maps.android.compose.Marker
import com.google.maps.android.compose.rememberMarkerState
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false }
) {
mountains.forEach { mountain ->
Marker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
tag = mountain,
onClick = { marker ->
onMountainClick(marker)
false
},
zIndex = if (mountain.is14er()) 5f else 2f
)
}
}
Go ahead and run the app you will see the markers you just added!
Customize markers
There are several customization options for markers you have just added to help them stand out and convey useful information to users. In this task, you'll explore some of those by customizing the image of each marker.
The starter project includes a helper function, vectorToBitmap
, to create to BitmapDescriptor
s from a @DrawableResource
.
The starter code includes a mountain icon, baseline_filter_hdr_24.xml
, that you will use to customize the markers.
The vectorToBitmap
function converts a vector drawable into a BitmapDescriptor
for use with the maps library. The icon colors are set using a BitmapParameters
instance.
data class BitmapParameters(
@DrawableRes val id: Int,
@ColorInt val iconColor: Int,
@ColorInt val backgroundColor: Int? = null,
val backgroundAlpha: Int = 168,
val padding: Int = 16,
)
fun vectorToBitmap(context: Context, parameters: BitmapParameters): BitmapDescriptor {
// ...
}
Use the vectorToBitmap
function to create two customized BitmapDescriptor
s; one for fourteeners and one for regular mountains. Then use the icon
parameter of Marker
composable to set the icon. Also, set the anchor
parameter to change the anchor location relative to the icon. Using the center works better for these circular icons.
@Composable
@GoogleMapComposable
fun BasicMarkersMapContent(
// ...
) {
// Create mountainIcon and fourteenerIcon
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.secondary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.toArgb(),
)
)
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
backgroundColor = MaterialTheme.colorScheme.primary.toArgb(),
)
)
mountains.forEach { mountain ->
val icon = if (mountain.is14er()) fourteenerIcon else mountainIcon
Marker(
// ...
anchor = Offset(0.5f, 0.5f),
icon = icon,
)
}
}
Run the app and marvel at the customized markers. Toggle the Show all
switch to see the full set of mountains. The mountains will have different markers depending on the mountain is a fourteener.
10. Advanced markers
AdvancedMarker
s add extra features to basic Markers
. In this step, you will set the collision behavior and configure the pin style.
Add @GoogleMapComposable
to the AdvancedMarkersMapContent
function. Loop over the mountains
adding an AdvancedMarker
for each.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
mountains.forEach { mountain ->
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
Notice the collisionBehavior
parameter. By setting this parameter to REQUIRED_AND_HIDES_OPTIONAL
, your marker will replace any lower priority marker. You can see this by zoom in on a basic marker compared to an advanced marker. The basic marker will likely have both your marker and marker placed in the same location in the base map. The advanced marker will cause the lower priority marker to be hidden.
Run the app to see the Advanced markers. Be sure to select the Advanced markers
tab in the bottom navigation row.
Customized AdvancedMarkers
The icons use the primary and secondary color schemes to distinguish between the fourteeners and other mountains. Use the vectorToBitmap
function to create two BitmapDescriptor
s; one for the fourteeners and one for the other mountains. Use those icons to create a custom pinConfig
for each type. Finally, apply the pin to corresponding AdvancedMarker
based on the is14er()
function.
@Composable
@GoogleMapComposable
fun AdvancedMarkersMapContent(
mountains: List<Mountain>,
onMountainClick: (Marker) -> Boolean = { false },
) {
val mountainIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onSecondary.toArgb(),
)
)
val mountainPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(mountainIcon))
setBackgroundColor(MaterialTheme.colorScheme.secondary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onSecondary.toArgb())
build()
}
val fourteenerIcon = vectorToBitmap(
LocalContext.current,
BitmapParameters(
id = R.drawable.baseline_filter_hdr_24,
iconColor = MaterialTheme.colorScheme.onPrimary.toArgb(),
)
)
val fourteenerPin = with(PinConfig.builder()) {
setGlyph(PinConfig.Glyph(fourteenerIcon))
setBackgroundColor(MaterialTheme.colorScheme.primary.toArgb())
setBorderColor(MaterialTheme.colorScheme.onPrimary.toArgb())
build()
}
mountains.forEach { mountain ->
val pin = if (mountain.is14er()) fourteenerPin else mountainPin
AdvancedMarker(
state = rememberMarkerState(position = mountain.location),
title = mountain.name,
snippet = mountain.elevation.toElevationString(),
collisionBehavior = AdvancedMarkerOptions.CollisionBehavior.REQUIRED_AND_HIDES_OPTIONAL,
pinConfig = pin,
onClick = { marker ->
onMountainClick(marker)
false
}
)
}
}
11. Clustered markers
In this step, you will use the Clustering
composable to add zoom-based item grouping.
The Clustering
composable requires a collection of ClusterItem
s. MountainClusterItem
implements the ClusterItem
interface. Add this class to the ClusteringMarkersMapContent.kt
file.
data class MountainClusterItem(
val mountain: Mountain,
val snippetString: String
) : ClusterItem {
override fun getPosition() = mountain.location
override fun getTitle() = mountain.name
override fun getSnippet() = snippetString
override fun getZIndex() = 0f
}
Now add the code to create MountainClusterItem
s from the list of mountains. Note this code uses a UnitsConverter
to convert to display units appropriate for the user based on their locale. This is set up in the MainActivity
using a CompositionLocal
@OptIn(MapsComposeExperimentalApi::class)
@Composable
@GoogleMapComposable
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
val unitsConverter = LocalUnitsConverter.current
val resources = LocalContext.current.resources
val mountainClusterItems by remember(mountains) {
mutableStateOf(
mountains.map { mountain ->
MountainClusterItem(
mountain = mountain,
snippetString = unitsConverter.toElevationString(resources, mountain.elevation)
)
}
)
}
Clustering(
items = mountainClusterItems,
)
}
And with that code, the markers are clustered based on the zoom-level. Nice and tidy!
Customize clusters
As with the other marker types, clustered markers are customizable. The clusterItemContent
parameter of the Clustering
composable sets a custom composable block to render a non-clustered item. Implement a @Composable
function to create the marker. The SingleMountain
function renders a composable Material 3 Icon
with a customized background color scheme.
In ClusteringMarkersMapContent.kt
, create a data class defining the color scheme for a marker:
data class IconColor(val iconColor: Color, val backgroundColor: Color, val borderColor: Color)
Also, in ClusteringMarkersMapContent.kt
create a composable function to render an icon for a given color scheme:
@Composable
private fun SingleMountain(
colors: IconColor,
) {
Icon(
painterResource(id = R.drawable.baseline_filter_hdr_24),
tint = colors.iconColor,
contentDescription = "",
modifier = Modifier
.size(32.dp)
.padding(1.dp)
.drawBehind {
drawCircle(color = colors.backgroundColor, style = Fill)
drawCircle(color = colors.borderColor, style = Stroke(width = 3f))
}
.padding(4.dp)
)
}
Now create a color scheme for fourteeners and another color scheme for other mountains. In the clusterItemContent
block, select the color scheme based on whether or not the given mountain is a fourteener.
fun ClusteringMarkersMapContent(
mountains: List<Mountain>,
// ...
) {
// ...
val backgroundAlpha = 0.6f
val fourteenerColors = IconColor(
iconColor = MaterialTheme.colorScheme.onPrimary,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.primary
)
val otherColors = IconColor(
iconColor = MaterialTheme.colorScheme.secondary,
backgroundColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = backgroundAlpha),
borderColor = MaterialTheme.colorScheme.secondary
)
// ...
Clustering(
items = mountainClusterItems,
clusterItemContent = { mountainItem ->
val colors = if (mountainItem.mountain.is14er()) {
fourteenerColors
} else {
otherColors
}
SingleMountain(colors)
},
)
}
Now, run the app to see customized versions of the individual items.
12. Draw on the map
While you have already explored one way to draw on the map (by adding markers), the Maps SDK for Android supports numerous other ways you can draw to display useful information on the map.
For example, if you wanted to represent routes and areas on the map, you can use Polyline
s and Polygon
s to display these on the map. Or, if you want to fix an image to the ground's surface, you can use a GroundOverlay
.
In this task, you learn how to draw shapes, specifically an outline around the State of Colorado. The Colorado border is defined as between 37°N and 41°N latitude and 102°03'W and 109°03'W. This makes drawing the outline pretty straightforward.
The starter code includes a DMS
class to convert from degrees-minutes-seconds notation to decimal degrees.
enum class Direction(val sign: Int) {
NORTH(1),
EAST(1),
SOUTH(-1),
WEST(-1)
}
/**
* Degrees, minutes, seconds utility class
*/
data class DMS(
val direction: Direction,
val degrees: Double,
val minutes: Double = 0.0,
val seconds: Double = 0.0,
)
fun DMS.toDecimalDegrees(): Double =
(degrees + (minutes / 60) + (seconds / 3600)) * direction.sign
With the DMS class, you can draw Colorado's border by defining the four corner LatLng
locations and rendering those as a Polygon
s. Add the following code to MountainMap.kt
@Composable
@GoogleMapComposable
fun ColoradoPolygon() {
val north = 41.0
val south = 37.0
val east = DMS(WEST, 102.0, 3.0).toDecimalDegrees()
val west = DMS(WEST, 109.0, 3.0).toDecimalDegrees()
val locations = listOf(
LatLng(north, east),
LatLng(south, east),
LatLng(south, west),
LatLng(north, west),
)
Polygon(
points = locations,
strokeColor = MaterialTheme.colorScheme.tertiary,
strokeWidth = 3F,
fillColor = MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f),
)
}
Now call ColoradoPolyon()
inside the GoogleMap
content block.
@Composable
fun MountainMap(
// ...
) {
Box(
// ...
) {
GoogleMap(
// ...
) {
ColoradoPolygon()
}
}
}
Now the app outlines the State of Colorado while giving it a subtle fill.
13. Add a KML layer and scale bar
In this final section you will roughly outline the different mountain ranges and add a scale bar to the map.
Outline the mountain ranges
Previously, you drew an outline around Colorado. Here you are going to add more complex shapes to the map. The starter code includes a Keyhole Markup Language, or KML, file which roughly outlines the important mountains ranges. The Maps SDK for Android Utility Library has a function to add a KML layer to the map. In MountainMap.kt
add a MapEffect
call in the GoogleMap
content block after the when
block. The MapEffect
function is called with a GoogleMap
object. It can serve as a useful bridge between non-composable APIs and libraries which require a GoogleMap
object.
fun MountainMap(
// ...
) {
var isMapLoaded by remember { mutableStateOf(false) }
val context = LocalContext.current
GoogleMap(
// ...
) {
// ...
when (selectedMarkerType) {
// ...
}
// This code belongs inside the GoogleMap content block, but outside of
// the 'when' statement
MapEffect(key1 = true) {map ->
val layer = KmlLayer(map, R.raw.mountain_ranges, context)
layer.addLayerToMap()
}
}
Add a map scale
As your final task, you will add a scale to the map. The ScaleBar
implements a scale composable that can be added to the map. Note, that the ScaleBar
is not a
@GoogleMapComposable
and therefore cannot be added to the GoogleMap
content. You instead add it to the Box
that holds the map.
Box(
// ...
) {
GoogleMap(
// ...
) {
// ...
}
ScaleBar(
modifier = Modifier
.padding(top = 5.dp, end = 15.dp)
.align(Alignment.TopEnd),
cameraPositionState = cameraPositionState
)
// ...
}
Run the app to see the fully implemented codelab.
14. Get the solution code
To download the code for the finished codelab, you can use these commands:
- Clone the repository if you have
git
installed.
$ git clone https://github.com/googlemaps-samples/codelab-maps-platform-101-compose.git
Alternatively, you can click the following button to download the source code.
- Upon getting the code, go ahead and open the project found inside the
solution
directory in Android Studio.
15. Congratulations
Congratulations! You covered a lot of content and hopefully you have a better understanding of the core features offered in the Maps SDK for Android.
Learn more
- Maps SDK for Android - Build dynamic, interactive, customized maps, location, and geospatial experiences for your Android apps.
- Maps Compose Library - a set of open source composable functions and data types that you can use with Jetpack Compose to build your app.
- android-maps-compose - sample code on GitHub demonstrating all the features covered in this codelab and more.
- More Kotlin codelabs for building Android apps with Google Maps Platform