1. Before you begin
Building apps with Google Maps allows you to add features to your app, such as satellite imagery, robust UI controls for maps, location tracking, and location markers. You can add value to the standard Google Maps by showing information from your own dataset, such as the locations of well-known fishing or climbing areas. You can also create games in which the player explores the physical world, like in a treasure hunt or even augmented reality games.
In this lesson, you create a Google Maps app called Wander that displays customized maps and shows the user's location.
Prerequisites
Knowledge of the following:
- How to create a basic Android app and run it using Android Studio.
- How to create and manage resources, such as strings.
- How to refactor code and rename variables using Android Studio.
- How to use a Google map as a user.
- How to set runtime permissions.
What you'll learn
- How to get an API key from the Google API Console and register the key to your app
- How to integrate a Google Map in your app
- How to display different map types
- How to style the Google Map
- How to add markers to your map
- How to enable the user to place a marker on a point of interest (POI)
- How to enable location tracking
- How to create The
Wander
app, which has an embedded Google Map - How to create custom features for your app, such as markers and styling
- How to enable location tracking in your app
2. App overview
In this codelab, you create the Wander
app, which displays a Google map with custom styling. The Wander app allows you to drop markers onto locations, add overlays, and see your location in real time.
3. Task: Set up the project and get an API Key
The Maps SDK for Android requires an API key. To obtain the API key, register your project in the API & Services page. The API key is tied to a digital certificate that links the app to its author. For more information about using digital certificates and signing your app, see Sign your app.
In this codelab, you use the API key for the debug certificate. The debug certificate is insecure by design, as described in Sign your debug build. Published Android apps that use the Maps SDK for Android require a second API key: the key for the release certificate. For more information about obtaining a release certificate, see Get an API Key.
Android Studio includes a Google Maps Activity template, which generates helpful template code. The template code includes a google_maps_api.xml file containing a link that simplifies obtaining an API key.
Step 1: Create the Wander project with the maps template
- Create a new Android Studio project.
- Select the Google Maps Activity template.
- Name the project
Wander
. - Set the minimum API level to API 19. Make sure the language is Kotlin.
- Click Finish.
- Once the app is done building, take a look at your project and the following maps-related files that Android Studio creates for you:
google_maps_api.xml—You use this configuration file to hold your API key. The template generates two google_maps_api.xml files: one for debug and one for release. The file for the API key for the debug certificate is located in src/debug/res/values. The file for the API key for the release certificate is located in src/release/res/values. In this codelab, you only use the debug certificate.
activity_maps.xml—This layout file contains a single fragment that fills the entire screen. The SupportMapFragment
class is a subclass of the Fragment
class. A SupportMapFragment
is the simplest way to place a map in an app. It's a wrapper around a view of a map to automatically handle the necessary lifecycle needs.
You can include SupportMapFragment
in a layout file using a <fragment>
tag in any ViewGroup
, with an additional name
attribute.
android:name="com.google.android.gms.maps.SupportMapFragment"
MapsActivity.java—The MapsActivity.kt file instantiates the SupportMapFragment
in the onCreate()
method, and uses the class' getMapAsync
()
to automatically initialize the maps system and the view. The activity that contains the SupportMapFragment
must implement the OnMapReadyCallback
interface and that interface's onMapReady()
method. The onMapReady()
method is called when the map is loaded.
Step 2: Obtain the API key
- Open the debug version of the google_maps_api.xml file.
- In the file, look for a comment with a long URL. The URL's parameters include specific information about your app.
- Copy and paste the URL into a browser.
- Follow the prompts to create a project on the APIs & Services page. Because of the parameters in the provided URL, the page knows to automatically enable the Maps SDK for Android.
- Click Create an API Key.
- On the next page, go to the API Keys section and click the key you just created.
- Click Restrict Key and select Maps SDK for Android to restrict the key's use to Android apps.
- Copy the generated API key. It starts with "
AIza"
. - In the
google_maps_api.xml
file, paste the key into thegoogle_maps_key
string where it saysYOUR_KEY_HERE
. - Run your app. You should see an embedded map in your activity with a marker set in Sydney, Australia. (The Sydney marker is part of the template and you change it later.)
Step 3: Rename mMap
MapsActivity
has a private lateinit
var
called mMap
, which is of type GoogleMap
. To follow Kotlin naming conventions, change the name of mMap
to map
.
- In
MapsActivity
, right-clickmMap
and click Refactor > Rename...
- Change the variable name to
map
.
Notice how all the references to mMap
in the onMapReady()
function also change to map
.
4. Task: Add map types
Google Maps includes several map types: normal, hybrid, satellite, terrain, and "none" (for no map at all).
Normal map | Satellite map | Hybrid map | Terrain map |
Each type of map provides different kinds of information. For example, when using maps for navigation in a car, it's helpful to see street names, so you could use the normal option. When you are hiking, the terrain map could be helpful to decide how much more you have to climb to get to the top.
In this task you:
- Add an app bar with an options menu that allows the user to change the map type.
- Move the map's starting location to your own home location.
- Add support for markers, which indicate single locations on a map and can include a label.
Add menu for map types
In this step, you add an app bar with an options menu that allows the user to change the map type.
- To create a new menu XML file, right-click your res directory and select New > Android Resource File.
- In the dialog, name the file
map_options
. - Choose Menu for the resource type.
- Click OK.
- In the Code tab, replace the code in the new file with the following code to create the map menu options. The "none" map type is omitted because "none" results in the lack of any map at all. This step causes an error, but you resolve it in the next step.
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/normal_map"
android:title="@string/normal_map"
app:showAsAction="never" />
<item
android:id="@+id/hybrid_map"
android:title="@string/hybrid_map"
app:showAsAction="never" />
<item
android:id="@+id/satellite_map"
android:title="@string/satellite_map"
app:showAsAction="never" />
<item
android:id="@+id/terrain_map"
android:title="@string/terrain_map"
app:showAsAction="never" />
</menu>
- In
strings.xml
, add resources for thetitle
attributes in order to resolve the errors.
<resources>
...
<string name="normal_map">Normal Map</string>
<string name="hybrid_map">Hybrid Map</string>
<string name="satellite_map">Satellite Map</string>
<string name="terrain_map">Terrain Map</string>
<string name="lat_long_snippet">Lat: %1$.5f, Long: %2$.5f</string>
<string name="dropped_pin">Dropped Pin</string>
<string name="poi">poi</string>
</resources>
- In
MapsActivity
, override theonCreateOptionsMenu()
method and inflate the menu from themap_options
resource file.
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.map_options, menu)
return true
}
- In
MapsActivity.kt
, override theonOptionsItemSelected()
method. Change the map type using map-type constants to reflect the user's selection.
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
// Change the map type based on the user's selection.
R.id.normal_map -> {
map.mapType = GoogleMap.MAP_TYPE_NORMAL
true
}
R.id.hybrid_map -> {
map.mapType = GoogleMap.MAP_TYPE_HYBRID
true
}
R.id.satellite_map -> {
map.mapType = GoogleMap.MAP_TYPE_SATELLITE
true
}
R.id.terrain_map -> {
map.mapType = GoogleMap.MAP_TYPE_TERRAIN
true
}
else -> super.onOptionsItemSelected(item)
}
- Run the app.
- Click to change the map type. Notice how the map's appearance changes between the different modes.
5. Task: Add markers
By default, the onMapReady()
callback includes code that places a marker in Sydney, Australia, where Google Maps was created. The default callback also animates the map to pan to Sydney.
In this task, you make the map's camera move to your home, zoom to a level you specify, and place a marker there.
Step 1: Zoom to your home and add a marker
- In the
MapsActivity.kt
file, find theonMapReady()
method. Remove the code in it that places the marker in Sydney and moves the camera. This is what your method should look like now.
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
}
- Find the latitude and longitude of your home by following these instructions.
- Create a value for the latitude and a value for the longitude, and input their float values.
val latitude = 37.422160
val longitude = -122.084270
- Create a new
LatLng
object calledhomeLatLng
. In thehomeLatLng
object, pass in the values you just created.
val homeLatLng = LatLng(latitude, longitude)
- Create a
val
for how zoomed in you want to be on the map. Use zoom level 15f.
val zoomLevel = 15f
The zoom level controls how zoomed in you are on the map. The following list gives you an idea of what level of detail each level of zoom shows:
1
: World5
: Landmass/continent10
: City15
: Streets20
: Buildings
- Move the camera to
homeLatLng
by calling themoveCamera()
function on themap
object and pass in aCameraUpdate
object usingCameraUpdateFactory.newLatLngZoom()
. Pass in thehomeLatLng
object and thezoomLevel
.
map.moveCamera(CameraUpdateFactory.newLatLngZoom(homeLatLng, zoomLevel))
- Add a marker to the map at
homeLatLng
.
map.addMarker(MarkerOptions().position(homeLatLng))
Your final method should look like this:
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
//These coordinates represent the latitude and longitude of the Googleplex.
val latitude = 37.422160
val longitude = -122.084270
val zoomLevel = 15f
val homeLatLng = LatLng(latitude, longitude)
map.moveCamera(CameraUpdateFactory.newLatLngZoom(homeLatLng, zoomLevel))
map.addMarker(MarkerOptions().position(homeLatLng))
}
- Run your app. The map should pan to your home, zoom to the desired level, and place a marker on your home.
Step 2: Allow users to add a marker using a long click
In this step, you add a marker when the user touches and holds a location on the map.
- Create a method stub in
MapsActivity
calledsetMapLongClick()
that takes aGoogleMap
as an argument. - Attach a
setOnMapLongClickListener
listener to the map object.
private fun setMapLongClick(map:GoogleMap) {
map.setOnMapLongClickListener { }
}
- In
setOnMapLongClickListener()
, call theaddMarker()
method. Pass in a newMarkerOptions
object with the position set to the passed-inLatLng
.
private fun setMapLongClick(map: GoogleMap) {
map.setOnMapLongClickListener { latLng ->
map.addMarker(
MarkerOptions()
.position(latLng)
)
}
}
- At the end of the
onMapReady()
method, callsetMapLongClick()
withmap
.
override fun onMapReady(googleMap: GoogleMap) {
...
setMapLongClick(map)
}
- Run your app.
- Touch and hold the map to place a marker at a location.
- Tap the marker, which centers it on the screen.
Step 3: Add an info window for the marker
In this step, you add an InfoWindow
that displays the coordinates of the marker when the marker is tapped.
- In
setMapLongClick()setOnMapLongClickListener()
, create aval
forsnippet
. A snippet is additional text displayed after the title. Your snippet displays the latitude and longitude of a marker.
private fun setMapLongClick(map: GoogleMap) {
map.setOnMapLongClickListener { latLng ->
// A snippet is additional text that's displayed after the title.
val snippet = String.format(
Locale.getDefault(),
"Lat: %1$.5f, Long: %2$.5f",
latLng.latitude,
latLng.longitude
)
map.addMarker(
MarkerOptions()
.position(latLng)
)
}
}
- In
addMarker()
, set thetitle
of the marker to Dropped Pin using aR.string.
dropped_pin
string resource. - Set the marker's
snippet
tosnippet
.
The completed function looks like this:
private fun setMapLongClick(map: GoogleMap) {
map.setOnMapLongClickListener { latLng ->
// A Snippet is Additional text that's displayed below the title.
val snippet = String.format(
Locale.getDefault(),
"Lat: %1$.5f, Long: %2$.5f",
latLng.latitude,
latLng.longitude
)
map.addMarker(
MarkerOptions()
.position(latLng)
.title(getString(R.string.dropped_pin))
.snippet(snippet)
)
}
}
- Run your app.
- Touch and hold the map to drop a location marker.
- Tap the marker to show the info window.
Step 4: Add POI listener
By default, points of interest (POIs) appear on the map along with their corresponding icons. POIs include parks, schools, government buildings, and more. When the map type is set to normal
, business POIs also appear on the map. Business POIs represent businesses, such as shops, restaurants, and hotels.
In this step, you add a GoogleMap.OnPoiClickListener
to the map. This click listener places a marker on the map immediately when the user clicks a POI. The click listener also displays an info window that contains the POI name.
- Create a method stub in
MapsActivity
calledsetPoiClick()
that takes aGoogleMap
as an argument. - In the
setPoiClick()
method, set anOnPoiClickListener
on the passed-inGoogleMap
.
private fun setPoiClick(map: GoogleMap) {
map.setOnPoiClickListener { poi ->
}
}
- In the
setOnPoiClickListener()
, create aval poiMarker
for the marker . - Set it to a marker using
map.addMarker()
withMarkerOptions
setting thetitle
to the name of the POI.
private fun setPoiClick(map: GoogleMap) {
map.setOnPoiClickListener { poi ->
val poiMarker = map.addMarker(
MarkerOptions()
.position(poi.latLng)
.title(poi.name)
)
}
}
- In the
setOnPoiClickListener()
function, callshowInfoWindow()
onpoiMarker
to immediately show the info window.
poiMarker.showInfoWindow()
Your final code for the setPoiClick()
function should look like this.
private fun setPoiClick(map: GoogleMap) {
map.setOnPoiClickListener { poi ->
val poiMarker = map.addMarker(
MarkerOptions()
.position(poi.latLng)
.title(poi.name)
)
poiMarker.showInfoWindow()
}
}
- At the end of
onMapReady()
, callsetPoiClick()
and pass inmap
.
override fun onMapReady(googleMap: GoogleMap) {
...
setPoiClick(map)
}
- Run your app and find a POI, such as a park or a coffee shop.
- Tap the POI to place a marker on it and display the POI's name in an info window.
6. Task: Style your map
You can customize Google Maps in many ways, giving your map a unique look and feel.
You can customize a MapFragment
object using the available XML attributes, as you would customize any other fragment. However, in this step, you customize the look and feel of the content of the MapFragment
, using methods on the GoogleMap
object.
To create a customized style for your map, you generate a JSON file that specifies how features in the map are displayed. You don't have to create this JSON file manually. Google provides the Maps Platform Styling Wizard, which generates the JSON for you after you visually style your map. In this task, you style the map with a retro theme, meaning that the map uses vintage colors and you add colored roads.
Step 1: Create a style for your map
- Navigate to https://mapstyle.withgoogle.com/ in your browser.
- Select Create a Style.
- Select Retro.
- Click More Options.
- Select Road > Fill.
- Change the color of the roads to any color you choose (such as pink).
- Click Finish.
- Copy the JSON code from the resulting dialog and, if you wish, stash it in a plain text note for use in the next step.
Step 2: Add the style to your map
- In Android Studio, in the
res
directory, create a resource directory and name itraw
. You use theraw
directory resources like JSON code. - Create a file in
res/raw
calledmap_style.json
. - Paste your stashed JSON code into the new resource file.
- In
MapsActivity
, create aTAG
class variable above theonCreate()
method. This is used for logging purposes.
private val TAG = MapsActivity::class.java.simpleName
- Also in
MapsActivity
, create asetMapStyle()
function that takes in aGoogleMap
. - In
setMapStyle()
, add atry{}
block. - In the
try{}
block, create aval success
for the success of styling. (You add the following catch block.) - In the
try{}
block, set the JSON style to the map, callsetMapStyle()
on theGoogleMap
object. Pass in aMapStyleOptions
object, which loads the JSON file. - Assign the result to
success
. ThesetMapStyle()
method returns a boolean indicating the success status of parsing the styling file and setting the style.
private fun setMapStyle(map: GoogleMap) {
try {
// Customize the styling of the base map using a JSON object defined
// in a raw resource file.
val success = map.setMapStyle(
MapStyleOptions.loadRawResourceStyle(
this,
R.raw.map_style
)
)
}
}
- Add an if statement for
success
being false. If the styling is unsuccessful, print a log that the parsing has failed.
private fun setMapStyle(map: GoogleMap) {
try {
...
if (!success) {
Log.e(TAG, "Style parsing failed.")
}
}
}
- Add a
catch{}
block to handle the situation of a missing style file. In thecatch
block, if the file can't be loaded, then throw aResources.NotFoundException
.
private fun setMapStyle(map: GoogleMap) {
try {
...
} catch (e: Resources.NotFoundException) {
Log.e(TAG, "Can't find style. Error: ", e)
}
}
The finished method should look like the following code snippet:
private fun setMapStyle(map: GoogleMap) {
try {
// Customize the styling of the base map using a JSON object defined
// in a raw resource file.
val success = map.setMapStyle(
MapStyleOptions.loadRawResourceStyle(
this,
R.raw.map_style
)
)
if (!success) {
Log.e(TAG, "Style parsing failed.")
}
} catch (e: Resources.NotFoundException) {
Log.e(TAG, "Can't find style. Error: ", e)
}
}
- Finally, call the
setMapStyle()
method in theonMapReady()
method passing in yourGoogleMap
object.
override fun onMapReady(googleMap: GoogleMap) {
...
setMapStyle(map)
}
- Run your app.
- Set the map to
normal
mode and the new styling should be visible with retro theming and roads of your chosen color.
Step 3: Style your marker
You can personalize your map further by styling the map markers. In this step, you change the default red markers into something more groovy.
- In the
onMapLongClick()
method, add the following line of code to theMarkerOptions()
of the constructor to use the default marker, but change the color to blue.
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))
Now onMapLongClickListener()
looks like this:
map.setOnMapLongClickListener { latLng ->
// A snippet is additional text that's displayed after the title.
val snippet = String.format(
Locale.getDefault(),
"Lat: %1$.5f, Long: %2$.5f",
latLng.latitude,
latLng.longitude
)
map.addMarker(
MarkerOptions()
.position(latLng)
.title(getString(R.string.dropped_pin))
.snippet(snippet)
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE))
)
}
- Run the app. The markers that appear after you long click are now shaded blue. Note that POI markers are still red because you didn't add styling to the
onPoiClick()
method.
7. Task: Add an overlay
One way you can customize the Google map is by drawing on top of it. This technique is useful if you want to highlight a particular type of location, such as popular fishing spots.
- Shapes: You can add polylines, polygons, and circles to the map.
GroundOverlay
objects: A ground overlay is an image that is fixed to a map. Unlike markers, ground overlays are oriented to the Earth's surface rather than to the screen. Rotating, tilting, or zooming the map changes the orientation of the image. Ground overlays are useful when you wish to fix a single image in one area on the map.
Step: Add a ground overlay
In this task, you add a ground overlay in the shape of an Android to your home location.
- Download this Android image and save it in your
res/drawable
folder. (Make sure the file name isandroid.png
.)
- In
onMapReady()
, after the call to move the camera to your home's position, create aGroundOverlayOptions
object. - Assign the object to a variable called
androidOverlay
.
val androidOverlay = GroundOverlayOptions()
- Use the
BitmapDescriptorFactory.fromResource()
method to create aBitmapDescriptor
object from the downloaded image resource. - Pass the resulting
BitmapDescriptor
object into theimage()
method of theGroundOverlayOptions
object.
val androidOverlay = GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromResource(R.drawable.android))
- Create a
float overlaySize
for the width in meters of the desired overlay. For this example, a width of100f
works well.
Set the position
property for the GroundOverlayOptions
object by calling the position()
method, and pass in the homeLatLng
object and the overlaySize
.
val overlaySize = 100f
val androidOverlay = GroundOverlayOptions()
.image(BitmapDescriptorFactory.fromResource(R.drawable.android))
.position(homeLatLng, overlaySize)
- Call
addGroundOverlay()
on theGoogleMap
object and pass in yourGroundOverlayOptions
object.
map.addGroundOverlay(androidOverlay)
- Run the app.
- Change the value of
zoomLevel
to 18f to see the Android image as an overlay.
8. Task: Enable location tracking
Users often use Google Maps to see their current location. To display the device location on your map, you can use the location-data layer.
The location-data layer adds My Location icon to the map.
When the user taps the button, the map centers on the device's location. The location is shown as a blue dot if the device is stationary and as a blue chevron if the device is moving.
In this task, you enable the location-data layer.
Step: Request location permissions
Enabling location tracking in Google Maps requires a single line of code. However, you must make sure that the user has granted location permissions (using the runtime-permission model).
In this step, you request location permissions and enable the location tracking.
- In the
AndroidManifest.xml
file, verify that theFINE_LOCATION
permission is already present. Android Studio inserted this permission when you selected the Google Maps template.
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
- In
MapsActivity
, create aREQUEST_LOCATION_PERMISSION
class variable.
private val REQUEST_LOCATION_PERMISSION = 1
- To check if permissions are granted, create a method in the
MapsActivity
calledisPermissionGranted()
. In this method, check if the user has granted the permission.
private fun isPermissionGranted() : Boolean {
return ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
- To enable location tracking in your app, create a method in
MapsActivity
calledenableMyLocation()
that takes no arguments and doesn't return anything. Inside, check for theACCESS_FINE_LOCATION
permission. If the permission is granted, enable the location layer. Otherwise, request the permission.
private fun enableMyLocation() {
if (isPermissionGranted()) {
map.isMyLocationEnabled = true
}
else {
ActivityCompat.requestPermissions(
this,
arrayOf<String>(Manifest.permission.ACCESS_FINE_LOCATION),
REQUEST_LOCATION_PERMISSION
)
}
}
- Call
enableMyLocation()
from theonMapReady()
callback to enable the location layer.
override fun onMapReady(googleMap: GoogleMap) {
...
enableMyLocation()
}
- Override the
onRequestPermissionsResult()
method. Check if therequestCode
is equal toREQUEST_LOCATION_PERMISSION
. If it is, that means that the permission is granted. If the permission is granted, also check if thegrantResults
array containsPackageManager.PERMISSION_GRANTED
in its first slot. If that is true, callenableMyLocation()
.
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray) {
if (requestCode == REQUEST_LOCATION_PERMISSION) {
if (grantResults.contains(PackageManager.PERMISSION_GRANTED)) {
enableMyLocation()
}
}
}
- Run your app. There should be a dialog requesting access to the device's location. Go ahead and allow permission.
The map now displays the device's current location using a blue dot. Notice that there is a location button. If you move the map away from your location and click this button, it centers the map back to the device's location.
9. Solution code
Download the code for the finished codelab.
$ git clone https://github.com/googlecodelabs/android-kotlin-geo-maps
Alternatively, you can download the repository as a zip file, unzip it and open it in Android Studio.
10. Summary
Congratulations! You added a Google map to an Android Kotlin app and styled it.
11. Learn more
Android developer documentation:
Reference documentation:
12. Next codelab
For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.