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:

App first run screen

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:

  1. Setting up the dependencies:
  2. 
      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.

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

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:

  1. You create a CoroutineScope variable you’ll use to show your Snackbar.
  2. This is a variable to get the context of your current composable.
  3. You have a Boolean variable showMap that represents whether the app has necessary permissions.
  4. Here, you call PermissionDialog, a custom composable that handles all the permissions logic.
  5. The PermissionDialog has a callback that returns which permission option the user has chosen. It can either be PermissionGranted or PermissionDenied. On each of this, you update the showMap 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:

Singapore location on map

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.

Singapore marker

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:

Singapore marker information window

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.

Singapore map with circle

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:

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

JSON map styling wizard

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:

Wizard with silver-theme styling

On the right side, you can see the map color changes to reflect the selected theme. Next, click MORE OPTIONS as shown above.

Styling wizard with more customization options

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.

Styling wizard advanced customization

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:

Styling wizard export style

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

Map with custom JSON style

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:

Geo Marker screen TODOs

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.

Geo Marker screen

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:

Geo Marker screen with instructions

Tap any point on the map.

Geo Marker screen with save location UI

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:

  1. 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 of LatLng to draw a polygon.
  2. Here, for each item in the areaPoints list, you add a marker on your map.
  3. You use Polygon composable, to draw your polygon. You pass in the points to draw and the colors for your polygon.
  4. 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.

Geo Marker full flow

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.

Google Map UI Tests

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