Add a map to your Android app (Kotlin with Compose)

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

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

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.

  1. In the Cloud Console , click the project drop-down menu and select the project that you want to use for this codelab.

  1. 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 .
  2. 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.

  1. 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.

  1. 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:

  1. In Android Studio, open your top-level build.gradle.kts file and add the following code to the dependencies element under buildscript .
      buildscript 
      
     { 
      
     dependencies 
      
     { 
      
     classpath 
     ( 
     "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1" 
     ) 
      
     } 
     } 
     
    
  2. Open your module-level build.gradle.kts file and add the following code to the plugins element.
      plugins 
      
     { 
      
     // ... 
      
     id 
     ( 
     "com.google.android.libraries.mapsplatform.secrets-gradle-plugin" 
     ) 
     } 
     
    
  3. In your module-level build.gradle.kts file, ensure that targetSdk and compileSdk are set to at least 34.
  4. Save the file and sync your project with Gradle .
  5. Open the secrets.properties file in your top-level directory, and then add the following code. Replace YOUR_API_KEY with your API key. Store your key in this file because secrets.properties is excluded from being checked into a version control system.
      MAPS_API_KEY 
     = 
     YOUR_API_KEY 
     
    
  6. Save the file.
  7. Create the local.defaults.properties file in your top-level directory, the same folder as the secrets.properties file, and then add the following code.
       
     MAPS_API_KEY 
     = 
     DEFAULT_API_KEY 
     
    
    The purpose of this file is to provide a backup location for the API key if the 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 a secrets.properties file locally to provide your API key.
  8. Save the file.
  9. In your AndroidManifest.xml file, go to com.google.android.geo.API_KEY and update the android: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 
     } 
     " 
      
     / 
    > 
    
  10. In Android Studio, open your module-level build.gradle.kts file and edit the secrets property. If the secrets property does not exist, add it.Edit the properties of the plugin to set propertiesFileName to secrets.properties , set defaultPropertiesFileName to local.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:

  1. Create a map ID.
  2. 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.

Extents button

  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:

  1. 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.

  1. 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
Create a Mobile Website
View Site in Mobile | Classic
Share by: