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.MAPS_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 Androiddependencies:
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 Marker s 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