Maps Compose Library Tutorial for Android: Getting Started
[ad_1]
Google Maps is a modern toolkit that provides detailed information about geographical regions. Today, it has more than a billion users per day.
However, it gets complicated when you want to use the former library, Maps SDK for Android, with Jetpack Compose. You must write complex and often large View interoperability code blocks to combine Jetpack Compose with the standard map UI component – MapView. This opposes one of Jetpack Compose’s primary objectives of being simple and precise. To solve this, Google created a new and simpler way of handling Google Maps in Jetpack Compose projects.
In February 2022, Google released the Maps Compose library. It is an open-source set of composable functions that simplify Google Maps implementation. Besides that, the library contains specific data types related to Maps SDK for Android suited to Jetpack Compose.
In this tutorial, you’ll build the GeoMarker app. The app allows you to use Maps Compose features like markers, circles and info windows. Additionally, you’ll also be able to mark points on your UI and be able to draw a polygon from chosen points.
During the process, you’ll learn:
- Setting up Google Maps in compose.
- Requesting location permissions.
- Adding markers, info windows and circles on your map.
- Adding custom map styling.
- Drawing polygons on your map.
- Testing some map features.
Getting Started
Download the starter project by clicking Download Materials at the top or bottom of the tutorial.
Open Android Studio Chipmunk or later and import the starter project. Build and run the project. You’ll see the following screens:
The app shows an empty screen with a ‘Mark Area’ floating action button at the bottom. You’ll display your map and other map components on this screen. You’ll also add the geo-marking functionality.
Setting Up
To start working on maps in compose, you must complete the following steps:
- Setting up the dependencies:
- Secondly, you need a Google Maps API key for you to be able to use any of Google Maps APIs. You can find instructions on how to get your key here. Once you have your key, proceed to add it to your local.properties file as follows:
MAPS_API_KEY=YOUR_API_KEY
implementation 'com.google.maps.android:maps-compose:2.4.0'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'com.google.android.gms:play-services-location:20.0.0'
The first is the Maps Compose library, and the other two are the Play Services maps SDK and location SDKs. Note that these dependencies already exist in the starter project, so there’s no need to re-add them.
Now that you have everything set, time to get your hands dirty with maps in compose. You’ll start by requesting location permissions for your app.
Requesting Location Permissions
Your app needs location permissions for you to be able to show maps. Head over to presentation/screens/MapScreenContent.kt. Replace //TODO Add Permissions
with:
// 1
val scope = rememberCoroutineScope()
// 2
val context = LocalContext.current
// 3
var showMap by rememberSaveable {
mutableStateOf(false)
}
// 4
PermissionDialog(
context = context,
permission = Manifest.permission.ACCESS_FINE_LOCATION,
permissionRationale = stringResource(id = R.string.permission_location_rationale),
snackbarHostState = snackbarHostState) { permissionAction ->
// 5
when (permissionAction) {
is PermissionAction.PermissionDenied -> {
showMap = false
}
is PermissionAction.PermissionGranted -> {
showMap = true
scope.launch {
snackbarHostState.showSnackbar("Location permission granted!")
}
fetchLocationUpdates.invoke()
}
}
}
To resolve errors, replace your imports at the top with:
import android.Manifest
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.android.composegeomarker.R
import com.android.composegeomarker.permissions.PermissionAction
import com.android.composegeomarker.permissions.PermissionDialog
import kotlinx.coroutines.launch
Here’s what the code above does:
- You create a
CoroutineScope
variable you’ll use to show your Snackbar. - This is a variable to get the context of your current composable.
- You have a Boolean variable
showMap
that represents whether the app has necessary permissions. - Here, you call
PermissionDialog
, a custom composable that handles all the permissions logic. - The
PermissionDialog
has a callback that returns which permission option the user has chosen. It can either bePermissionGranted
orPermissionDenied
. On each of this, you update theshowMap
variable. When the user grants the permission, you show a Snackbar with a “Location permission granted!” message and start the location updates.
With this, you’re ready to show locations on a map, and that’s the next step.
Displaying a Position in a Map
Navigate to presentation/composables/MapView.kt. You’ll see two TODOs that you’ll work on in a moment.
But before that, replace your imports with the following:
import android.content.Context
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.rememberCameraPositionState
Start by replacing // TODO add Camera Position State
with:
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(location, 16f)
}
In the code above, you create a CameraPositionState
instance, which holds the configurations for your map. In this case, you set your map’s location and zoom level.
Second, replace // TODO Add Google Map
with:
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
)
GoogleMap
is a container for a MapView
, to which you pass both values, modifier
and cameraPositionState
. And that’s all you need to show a single location on a map in compose :]
Last, you need to call your custom MapView
composable in your MapScreenContent.kt. You pass in the context and location as parameters. For an example, you’ll use a fixed location in Singapore. Go back to presentation/screens/MapScreenContent.kt and below PermissionDialog
add:
val currentLocation = LatLng(1.35, 103.87)
if (showMap) {
MapView(context, currentLocation)
}
Add the following imports to your import statements to resolve the errors.
import com.android.composegeomarker.presentation.composables.MapView
import com.google.android.gms.maps.model.LatLng
Here, you added the conditional to check whether your map should be displayed. Once the condition is met, you call MapView
passing in the context and current location.
Build and run the app:
The app now shows the location in Singapore on the map. In the next section, you’ll add a marker to this location.
Adding a Marker on the Map
Inside presentation/composables/MapView.kt, add a pair of curly braces to GoogleMap
composable and add the following in the block:
Marker(
state = MarkerState(position = location),
)
Add any missing imports by pressing Option-Return on a Mac or Alt-Enter on a Windows PC. Your final result will be:
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
) {
Marker(
state = MarkerState(position = location),
)
}
You add a marker in a map by adding child composables to GoogleMap
as contents. A Marker
requires a MarkerState
instance that observes marker state such as its position and information window.
Pass the Singapore location to MarkerState
and then build and run the app.
You can see the red marker for Singapore at the center of your map.
Often, you’ll need to show information when a user taps a marker. For that, you’ll have to add InfoWindow
to your map, which you’ll learn next.
Showing Map Information Windows
Head back to presentation/composables/MapView.kt and add this code below the cameraPositionState
variable:
val infoWindowState = rememberMarkerState(position = location)
You have now created a state variable for the marker properties and passed the location to this marker.
Next, below your Marker
composable, add:
MarkerInfoWindow(
state = infoWindowState,
title = "My location",
snippet = "Location custom info window",
content = {
CustomInfoWindow(title = it.title, description = it.snippet)
}
)
In the code above, you create your information window using MarkerInfoWindow
composable. You can customize your information window to your liking. You pass the state
, title
, snippet
and content
as parameters. Inside the content lambda, you call your custom composable with your information window custom view.
Build and run the app. Tap the Singapore marker, and you should see:
The information window displays on top of the marker with texts from the title
and snippet
you passed as parameters.
Drawing Circles on Your Map
So far, you’ve seen how to add markers and info windows to your map. In this section, you’ll add another shape, a Circle
.
In MapView.kt, add the following below MarkerInfoWindow
in the GoogleMap
composable:
Circle(
center = location,
fillColor = MaterialTheme.colorScheme.secondaryContainer,
strokeColor = MaterialTheme.colorScheme.secondaryContainer,
radius = 300.00
)
Resolve the MaterialTheme
missing imports by pressing Option-Return on a Mac or Alt-Enter on a PC.
Circle
is yet another map child composable and has several parameters. For this tutorial, you only need to assign values to:
- center – the
LatLng
that represents the center of this circle. - fillColor – fill color of the circle.
- strokeColor – color of the outer circle or stroke.
- radius – circle radius.
Build and run the app.
You can now see a blue circle at the center of your map. Its center is the Singapore location that you passed.
So far, you’ve drawn several shapes on your map. In the next section, you’ll learn how to customize your map’s appearance by adding a custom JSON map style.
Customizing the Appearance of Your Map
There are two map styling options available with maps:
- Cloud-based styling: This allows you to create and edit map styles without requiring any changes in your app. You make all the changes in the cloud console, which are reflected on your apps once you have a map ID.
- JSON based styling: Here, you create a map style on the old style wizard . Once you complete the customization, you can download the JSON file and add it to your map.
In this tutorial, you’ll be using JSON styling. You’ll create your custom style to add to the map in the next section.
Creating a Custom JSON Map Styling
Open your preferred browser and head to the old style wizard. You should see:
On the left, you have customization options such as changing the density of the features and changing the theme of your map.
Start by selecting the Silver theme as shown below:
On the right side, you can see the map color changes to reflect the selected theme. Next, click MORE OPTIONS as shown above.
This shows a list of features you can customize and visualize on the map. For this tutorial, you’ll customize the Road feature.
Follow these steps:
- Click the Road feature, which will open up the element type section on the right.
- The elements type section has a list of elements you can customize, which in this case are labels and geometry.
- Click the Geometry option and change the color as per your preference. You can see the color is immediately reflected on the map.
That’s all for now. You can add as many customization options as you wish. Click FINISH, and you’ll see the Export Style dialog as shown:
Click COPY JSON option. This copies the JSON style in your clipboard. You’re now a few steps away from applying the custom style to your compose map.
Navigate back to Android Studio. Right-click the res directory, choose New ▸ Android Resource Directory and select raw. In the new raw directory, create a file named map_style.json and paste the copied style here.
Now, you have the style ready for use. Next, you need to apply it to your map.
Applying Custom Style to Your Map
Head over to presentation/composables/MapView.kt. Below your infoWindowState
variable add:
val mapProperties by remember {
mutableStateOf(
MapProperties(
mapStyleOptions = MapStyleOptions.loadRawResourceStyle(context, R.raw.map_style)
)
)
}
Add any missing imports by pressing Option-Return on a Mac or Alt-Enter on a PC. As seen above, you create a new state variable of type MapProperties
. This variable holds properties you can change on the map. You pass the custom style to the mapStyleOptions
, which loads the style from the raw directory.
Next, add this variable mapProperties
as properties
parameter to your GoogleMap
. Your final result should be:
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
properties = mapProperties
) {
// Child Composables
}
Build and run the app.
You can see your map now applies the style from your JSON file.
Requesting Location Updates
Note: This section is optional. You can skip ahead to Marking Polygon Positions if you want to start adding your geo marking functionality. However, if you’d like to understand how to do location updates, you’re in the right place! The functionality is already in the starter project.
A common feature of maps on devices is the ability for them to update in real time. To do that here, You’ll use a callbackFlow
to request for location updates. Inside utils package you’ll find LocationUtils.kt file. The location callbackFlow
is as follows:
@SuppressLint("MissingPermission")
fun FusedLocationProviderClient.locationFlow() = callbackFlow {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
try {
trySend(result.lastLocation)
} catch (e: Exception) {
Log.e("Error", e.message.toString())
}
}
}
requestLocationUpdates(createLocationRequest(), callback, Looper.getMainLooper())
.addOnFailureListener { e ->
close(e)
}
awaitClose {
removeLocationUpdates(callback)
}
}
Here, you wrap your LocationCallback
in a callbackFlow
. In the callbackFlow
, callback
is called whenever you have location updates from requestLocationUpdates
. And finally, you clean up resources when your callback is removed inside awaitClose
.
Open up MainActivity.kt, and check out fetchLocationUpdates()
to see how it fetches location updates:
private fun fetchLocationUpdates() {
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
fusedLocationClient.locationFlow().collect {
it?.let { location ->
geoMarkerViewModel.setCurrentLatLng(LatLng(location.latitude, location.longitude))
}
}
}
}
}
This uses repeatOnLifecycle()
to collect safely from your Flow in the UI. You also pass the location to your viewmodel to share the latest value with your composable.
In the next section, you’ll see how to draw polygons on your map and finish the geo marking part of the app.
Marking Polygon Positions
There are two options available to create your geo marker:
- Drawing polylines: You use the location update feature to draw polylines as a user walks in a certain area. You draw polylines after a user updates their location at set intervals.
- Draw polygons: You draw polygons from a list of
LatLng
coordinates. For this tutorial, you’ll be using this option.
Head over to presentation/screens/GeoMarkerScreen.kt and you’ll see:
In this file, you have a GeoMarkerScreen
composable that has several map state variables defined. It has a Scaffold
inside where you have your GoogleMap
composable. You have three TODOs you’ll address in a moment.
Build and run the app. Tap Mark Area.
You can see the map and a button at the bottom of the map. You’ll be adding functionality for adding geo points by clicking any three points on the map.
To begin with, replace // TODO Add click listener
with:
if (!drawPolygon) {
showSavePoint = true
clickedLocation = it
}
Here, you do a conditional check to check whether the polygon is already drawn. When the condition isn’t satisfied, you update the showSavePoint
, which is a Boolean that determines whether to show the UI to save the clicked point. Clicking a map also returns a LatLng
of the clicked point. You assign this value to the clickedLocation
variable.
Next, replace // TODO Save Point UI
with:
if (showSavePoint) {
SaveGeoPoint(latLng = clickedLocation) {
showSavePoint = it.hideSavePointUi
areaPoints.add(it.point)
}
} else {
if (areaPoints.isEmpty()) {
Text(
modifier = Modifier
.fillMaxWidth(),
color = Color.Blue,
text = "Click any point on the map to mark it.",
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
)
}
}
Add any missing imports by pressing Option-Return on a Mac or Alt-Enter on a PC. You add another conditional check.
When showSavePoint
is true, you show the SaveGeoPoint
composable. SaveGeoPoint
is a custom composable with UI for saving the clicked point. You pass the clickedLocation
from the map click listener. When the condition evaluates to false, you show a text with instructions on how to mark points on the map.
Build and run the app. Navigate to the Geo Marker Screen once more. You’ll see:
Tap any point on the map.
You can see the UI to save the point on your map. It displays the LatLng
and the Save Point action which saves your point.
You’ll notice when you save three points that the Complete button at the bottom becomes active. Tap Complete. Nothing happens on the map; it only shows a reset button. Like me, you were expecting to see a polygon. Don’t worry. You’ll fix this behavior in a moment.
Replace // TODO Add Polygon
with:
// 1
if (drawPolygon && areaPoints.isNotEmpty()) {
// 2
areaPoints.forEach {
Marker(state = MarkerState(position = it))
}
// 3
Polygon(
points = areaPoints,
fillColor = Color.Blue,
strokeColor = Color.Blue
)
}
// 4
if (showSavePoint) {
Marker(state = MarkerState(position = clickedLocation))
}
Add any missing imports by pressing Option-Return on a Mac or Alt-Enter on a PC.
Here’s what the code above does:
- This is a conditional check to check whether the polygon is drawn. You also check if the
areaPoints
has values because you need a list ofLatLng
to draw a polygon. - Here, for each item in the
areaPoints
list, you add a marker on your map. - You use
Polygon
composable, to draw your polygon. You pass in the points to draw and the colors for your polygon. - This is a marker for each point you click on the map.
Build and run the app, then tap the marker area button and add three markers. Finally, tap the complete button.
Congratulations! You’ve been able to create a geo marker with a polygon. You can reset the map and draw as many polygons as you want.
Writing Map UI Tests
Tests are usually important in any piece of software. Google Map Compose library was not left behind in terms of writing tests for your map logic. To make it more interesting, it’s easier for you to write the UI tests for your map composables.
Head over to your androidTest directory and open GoogleMapTest.kt. The test class GoogleMapTest
only has a handy setup
method that runs before your tests run. It initializes a CameraPositionState
with a location and a zoom level.
Before writing your tests, you need to set up your map. Add the following method below the setup
method:
private fun loadMap() {
val countDownLatch = CountDownLatch(1)
composeTestRule.setContent {
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
onMapLoaded = {
countDownLatch.countDown()
}
)
}
val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS)
assertTrue("Map loaded", mapLoaded)
}
Replace your imports at the top with:
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.createComposeRule
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.GoogleMap
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
You have a CountDownLatch
to allow waiting for the map to load before doing any operation on the map. You set the content of your screen with the composeTestRule
. In the setContent
lambda, you add the GoogleMap
composable. You also pass the cameraPositionState
modifier
, and inside your onMapLoaded
, you start your countdown.
Lastly, you perform an assertion after waiting 30 seconds to check whether the map was loaded. You’ll use this method to initialize your map in consecutive tests.
You’ll now add tests to show the camera position and map zoom level are set to the correct values.
Add the following tests:
@Test
fun testCameraPosition() {
loadMap()
assertEquals(singapore, cameraPositionState.position.target)
}
@Test
fun testZoomLevel() {
loadMap()
assertEquals(cameraZoom, cameraPositionState.position.zoom)
}
In the code above, you have two tests: one for testing the camera position and the other for testing the zoom level of your map. In each of these tests, you call loadMap()
and then assert that the position and zoom level on the map is similar to your initial location. Run the test.
You can see all your tests run successfully!
Where to Go From Here?
Download the final project by clicking Download Materials at the top or bottom of the tutorial.
You can explore the drawing polyline option to demonstrate someone walking through a field. You can perhaps add more tests to test your map-related functionalities.
Check out the official Google Maps Compose documentation to learn more about maps in Compose. To learn more about testing your compose layouts, checkout the official testing documentation.
Hopefully, you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!
[ad_2]
Source link