Teach you step by step how to develop a map APP using COMPOSE~

Translated from: https://www.darrylbayliss.net/jetpack-compose-for-maps/

Foreword

It’s hard to imagine that Jetpack Compose 1.0 was released back in July 2021.

  • https://android-developers.googleblog.com/2021/07/jetpack-compose-announcement.html

Now two years later, 24% of the top 1,000 apps on Google Play have adopted the new technology Compose, and its influence is evident.

  • https://android-developers.googleblog.com/2023/05/whats-new-in-jetpack-compose.html

As a member of the MAD (Modern Android Development) philosophy, Jetpack Compose has indeed achieved a lot. But I noticed that there is a technical corner that has been ignored by everyone, and that is Map.

In fact, I haven’t been exposed to the Compose SDK for a while, but recently I suddenly discovered that Google Map has followed the steps of MAD and released its own Compose lib, which is very exciting.

  • https://cloud.google.com/blog/products/maps-platform/compose-maps-sdk-android-now-available

This is undoubtedly important good news for companies and employees engaged in the mapping and surveying industries. Because today’s mobile map market is worth $35.5 billion, and it is predicted that this value will surge to $87.7 billion by 2028, with a calculated compound annual growth rate (CAGR) of as high as 19.83%.

Why is this signal important?

Because a larger market means that companies will have more opportunities to gain revenue from mobile map applications.

In addition to common scenarios, the scope of applications also includes food, grocery delivery and ride-hailing services. And if you dig deeper, you’ll find that there are many less obvious, but really relevant scenes.

The following are the application scenarios I compiled after a simple search:

  • For smart cities, mobile maps are an excellent choice. It helps to grasp the changes in the heartbeat of the city and provide visual display to promote a better understanding and response to the challenges faced by the city. This target is not limited to city planners, but also includes emergency response organizations and even ordinary residents.
  • Resource management can also benefit from map schemes. From agriculture to fisheries, from mining to forestry, maps always provide those involved in this field with a perspective to help them make the right decisions for sustainable resource extraction.
  • Transportation also relies heavily on mapping technology. Not only consumer-oriented apps like Google Map and Uber, but also enterprise-oriented map functions. In addition, transit agencies can use maps to manage traffic, such as how to direct traffic to ease congestion.

As climate change and weather become increasingly unpredictable, mapping technology can also help weather agencies, emergency response units and wildlife conservationists better understand how the world is changing and what proactive steps can be taken to mitigate it. , reduce this change.

The world we live in is constantly producing new data and more and more data. Now it is time to learn how to put this huge data on a map.

Back to business, let’s get back to the topic of this article: how to use Google’s Compose Map lib to achieve this goal step by step!

1. Import COMPOSE MAP library

You need to import Google Maps for Compose lib according to the following configuration:

 dependencies {<!-- -->
   implementation "com.google.maps.android:maps-compose:2.11.4"
   implementation "com.google.android.gms:play-services-maps:18.1.0"
 
   // Optional Util Library
   implementation "com.google.maps.android:maps-compose-utils:2.11.4"
   implementation 'com.google.maps.android:maps-compose-widgets:2.11.4'
 
   // Optional Accompanist permissions to request permissions in compose
   implementation "com.google.accompanist:accompanist-permissions:0.31.5-beta"
 }

Google Maps for Compose is implemented based on Google Maps SDK, so you need to import the SDK additionally. In fact, developers do not need to directly use most of the objects provided in the SDK because the Compose lib has wrapped these objects into Composable functions.

The additional utils and widgets libs are optional, depending on your needs:

  • The utils library provides the function of displaying cluster markers on the map
  • Widgets provide additional UI components (more on this later)

Additionally, I’ve imported Accompanist‘s permissions library to show how to easily request location permissions required by the map. To clarify, Accompanist is an experimental library for Google to try out and gather feedback from developers for some features that are not yet part of Jetpack Compose.

Finally, you need to go to the following Google Developer Console to register the Google Maps SDK API key and configure it into the project.

  • https://console.cloud.google.com/projectselector2/google/maps-apis/credentials

Security Tip: Be sure to lock your API key in the Google Developer Console to ensure it only applies to your app to avoid other unauthorized use.

2. Display map interface

Presenting a basic map interface is easy:

 setContent {<!-- -->
     val hydePark = LatLng(51.508610, -0.163611)
     val cameraPositionState = rememberCameraPositionState {<!-- -->
         position = CameraPosition.fromLatLngZoom(hydePark, 10f)
     }
 
     GoogleMap(
         modifier = Modifier.fillMaxSize(),
         cameraPositionState = cameraPositionState) {<!-- -->
             Marker(
                 state = MarkerState(position = hydePark),
                 title = "Hyde Park",
                 snippet = "Marker in Hyde Park"
             )
         }
  }

First, create a LatLng object that points to a specific area and use it with rememberCameraPositionState to set the Camera’s initial position. When dragging the map by hand or controlling the map movement through code, this method will remember the current position of the map. If this method is not used, Compose will always display the map back to its original position when the state changes.

Next, call the GoogleMap composable function, passing in the size-related Modifier modifiers and CameraPosition state.

GoogleMap also provides a slot API to pass in additional Composable functions, using them to display additional data on the map.

For example, we add a Marker Composable, and then bind it to its data-related MarkerState, which specifies the title and description required for the Marker tag.

Run it and you’ll get a beautiful aerial view of West London with the Hyde Park marker.

3. Custom mark view

You can use the MarkerInfoWindowContent function to override the window view of a Marker marker. , and it also provides a slot-based API, which means that any composable function can be passed in to display custom view content.

 setContent {<!-- -->
     val hydePark = LatLng(51.508610, -0.163611)
     val cameraPositionState = rememberCameraPositionState {<!-- -->
         position = CameraPosition.fromLatLngZoom(hydePark, 10f)
     }
 
     GoogleMap(
         modifier = Modifier.fillMaxSize(),
         cameraPositionState = cameraPositionState) {<!-- -->
             MarkerInfoWindowContent(
                 state = MarkerState(position = hydePark),
                 title = "Hyde Park",
                 snippet = "Marker in Hyde Park"
             ) {<!-- --> marker ->
                 Column(horizontalAlignment = Alignment.CenterHorizontally) {<!-- -->
                     Text(
                         modifier = Modifier.padding(top = 6.dp),
                         text = marker.title ?: "",
                         fontWeight = FontWeight.Bold
                     )
                     Text("Hyde Park is a Grade I-listed parked in Westminster")
                     Image(
                         modifier = Modifier
                             .padding(top = 6.dp)
                             .border(
                                 BorderStroke(3.dp, color = Color.Gray),
                                 shape = RectangleShape
                             ),
                         painter = painterResource(id = R.drawable.hyde_park),
                         contentDescription = "A picture of hyde park"
                     )
                 }
             }
         }
 }

For example, we have customized a Compose layout: using Column to wrap the Text control that displays the title, the Text control that displays the description, and the Text control that displays the address image. Image control.

Run it and you will see that our customized window view will be displayed when the mark is clicked.

4. Display multiple tags

Displaying multiple markers is very simple, just pass as many Markers as needed. Let’s add markers to a few different parks in west London.

 setContent {<!-- -->
         val hydePark = LatLng(51.508610, -0.163611)
         val regentsPark = LatLng(51.531143, -0.159893)
         val primroseHill = LatLng(51.539556, -0.16076088)
         val cameraPositionState = rememberCameraPositionState {<!-- -->
             position = CameraPosition.fromLatLngZoom(hydePark, 10f)
         }
     
         GoogleMap(
             modifier = Modifier.fillMaxSize(),
             cameraPositionState = cameraPositionState) {<!-- -->
                 // Marker 1
                 Marker(
                     state = MarkerState(position = hydePark),
                     title = "Hyde Park",
                     snippet = "Marker in Hyde Park"
                 )
                 // Marker 2
                 Marker(
                     state = MarkerState(position = regentsPark),
                     title = "Regents Park",
                     snippet = "Marker in Regents Park"
                 )
                 // Marker 3
                 Marker(
                     state = MarkerState(position = primroseHill),
                     title = "Primrose Hill",
                     snippet = "Marker in Primrose Hill"
                 )
             }
      }

You can see that each marker successfully appears on the map.

5. Display cluster markers

The Maps app can become busy for a short period of time to quickly display the content the user needs. If we display up to 300 markers on a map, it will be difficult for users to grasp the key points on the map. And Google Maps and hardware devices suffer because they have to render each marker, which impacts device performance and battery life.

The solution is Clustering, a technique that groups Markers that are close to each other into a single Marker. This clustering operation is based on zoom levels:

  • When the map is zoomed out, the markers will be grouped together to form a cluster.
  • As you zoom in on the map, the cluster will break up into individual markers.

Google Maps for Compose provides Clustering combinable functions to meet this need, eliminating the need for developers to write complex sorting or filtering logic and easily completing clustering.

 setContent {<!-- -->
         val hydePark = LatLng(51.508610, -0.163611)
         val regentsPark = LatLng(51.531143, -0.159893)
         val primroseHill = LatLng(51.539556, -0.16076088)
     
         val crystalPalacePark = LatLng(51.42153, -0.05749)
         val greenwichPark = LatLng(51.476688, 0.000130)
         val lloydPark = LatLng(51.364188, -0.080703)
         val cameraPositionState = rememberCameraPositionState {<!-- -->
             position = CameraPosition.fromLatLngZoom(hydePark, 10f)
         }
     
         GoogleMap(
             modifier = Modifier.fillMaxSize(),
             cameraPositionState = cameraPositionState) {<!-- -->
     
                 val parkMarkers = remember {<!-- -->
                     mutableStateListOf(
                         ParkItem(hydepark, "Hyde Park", "Marker in hyde Park"),
                         ParkItem(regentspark, "Regents Park", "Marker in Regents Park"),
                         ParkItem(primroseHill, "Primrose Hill", "Marker in Primrose Hill"),
                         ParkItem(crystalPalacePark, "Crystal Palace", "Marker in Crystal Palace"),
                         ParkItem(greenwichPark, "Greenwich Park", "Marker in Greenwich Park"),
                         ParkItem(lloydPark, "Lloyd park", "Marker in Lloyd Park"),
                     )
                 }
     
                 Clustering(items = parkMarkers,
                 onClusterClick = {<!-- -->
                     // Handle when the cluster is tapped
                 }, onClusterItemClick = {<!-- --> marker ->
                     // Handle when a marker in the cluster is tapped
                 })
             }
     }
     
     data class ParkItem(
         val itemPosition: LatLng,
         val itemTitle: String,
         val itemSnippet: String) : ClusterItem {<!-- -->
             override fun getPosition(): LatLng =
                 itemPosition
     
             override fun getTitle(): String =
                 itemTitle
     
             override fun getSnippet(): String =
                 itemSnippet
     }

Please pay attention to the newly added ParkItem data class above. Because the ClusterItem interface passed to the Clustering function must implement the ClusterItem interface, we have to use this class to package the Marker information that needs to be clustered. Include: location, title, and description.

By zooming in and out, you can see that the clustering code takes effect.

6. Obtain location permission authorization

The display of the map is usually synchronized with the user’s real-time location, so the map app has reason to request permission to obtain the user’s location.

Respect user permissions. It can be said that location permissions are one of the most sensitive permissions for users. Clearly inform users why your app needs this permission and actively explain the benefits of having it. If an app has certain features that don’t require permissions at all, it can gain users’ goodwill.

Google provides official guidance on how to handle user location permissions and how to access location data in the background.

  • https://developer.android.com/training/location/request-update
  • https://developer.android.com/training/location/background

If, after thorough investigation, you still determine that your app requires user permissions to access the location, you can use the permissions library in the Accompanist library to handle this conveniently:

 // Don't forget to add the permissions to AndroidManifest.xml
     val allLocationPermissionState = rememberMultiplePermissionsState(
         listOf(android.Manifest.permission.ACCESS_COARSE_LOCATION,
                android.Manifest.permission.ACCESS_FINE_LOCATION)
     )
     
     // Check if we have location permissions
     if (!allLocationPermissionsState.allPermissionsGranted) {<!-- -->
         // Show a component to request permission from the user
         Column(
             horizontalAlignment = Alignment.CenterHorizontally,
             verticalArrangement = Arrangement.Center,
             modifier = Modifier
             .padding(horizontal = 36.dp)
             .clip(RoundedCornerShape(16.dp))
             .background(Color.white)
         ) {<!-- -->
             Text(
                 modifier = Modifier.padding(top = 6.dp),
                 textAlign = TextAlign.Center,
                 text = "This app functions 150% times better with percise location enabled"
             )
             Button(modifier = Modifier.padding(top = 12.dp), onClick = {<!-- -->
                 allLocationPermissionsState.launchMultiplePermissionsRequest()
             }) {<!-- -->
                 Text(text = "Grant Permission")
             }
         }
     }

As shown in the above code, first check whether the App has permission to access ACCESS_FINE_LOCATION or high-precision GPS.

If not granted, display a dialog box that explains to the user why the permission is needed and provides a button entry to migrate to the system that grants the permission.

7. Display map moving animation

Map apps usually require users to move the view through touch. Google Maps for Compose provides corresponding APIs for moving views, allowing developers to navigate the view to a specified area based on touch events.

Here we show the moving effect of the map by switching several Marker markers.

 Box(contentAlignment = Alignment.Center) {<!-- -->
         GoogleMap(
             modifier = Modifier.fillMaxSize(),
             cameraPositionState = cameraPositionState
         ) {<!-- -->
             Clustering(items = parkMarkers,
                 onClusterClick = {<!-- -->
                     // Handle when the click is tapped
                     false
                 }, onClusterItemClick = {<!-- --> marker ->
                     // Handle when the marker is tapped
                 })
     
             LaunchedEffect(key1 = "Animation") {<!-- -->
                 for (marker in parkMarkers) {<!-- -->
                     cameraPositionState.animate(
                         CameraUpdateFactory.newLatLngZoom(
                             marker.itemPosition, // LatLng
                             16.0f), // Zoom level
                           2000 // Animation duration in millis
                         ),
                         delay(4000L) // Delay in millis
                 }
             }
         }
     }

The key to the above code is LaunchedEffect, which will traverse all Markers and call cameraPositionState.animate() one by one to complete the operation of navigating to it. Among them, Camera will update the received data changes by using newLatLngZoom().

The parameters of this method are of type LatLng, which contain: float data representing the map zoom level and long data to set the animation duration.

Finally, to differentiate animations between Markers, use delay() to add a 4s pause between each animation.

8. Show additional street view

Google Maps for Compose can provide more than just an aerial map, showing a 360-degree view of a location when the app is granted access to Street View.

In code, you can use the StreetView composable function item to achieve:

 var selectedMarker: ParkItem? by remember {<!-- --> mutableStateOf(null) }
     
     if (selectedMarker != null) {<!-- -->
         StreetView(Modifier.fillMaxSize(), streetViewPanoramaOptionsFactory = {<!-- -->
             StreetViewPanoramaOptions().position(selectedMarker!!.position)
         })
     } else {<!-- -->
         Box(contentAlignment = Alignment.Center) {<!-- -->
             GoogleMap(
                 modifier = Modifier.fillMaxSize(),
                 cameraPositionState = cameraPositionState
             ) {<!-- -->
                 Clustering(items = parkMarkers,
                 onClusterClick = {<!-- -->
                     // Handle when the cluster is clicked
                     false
                 }, onClusterItemClick = {<!-- --> marker ->
                     // Handle when a marker in the cluster is clicked
                     selectedMarker = marker
                     false
                 })
             }
         }
     }

Whenever a Marker is clicked, the selectedMarker variable is assigned a value in the code, which means that a Marker is selected. At this time, the code will use the location information in the Marker to display the corresponding StreetView view.

9. Display drawn shapes/annotations

Developers may need to draw shapes and annotations on maps, and Google Maps for Compose accordingly provides many composable functions to achieve such operations.

Here we take the Circle composable function as an example.

If the app wants to show the user’s current location, a circle is a good representation. You can consider using a circle to represent the area where the user is active.

 Box(contentAlignment = Alignment.Center) {<!-- -->
             GoogleMap(
                 modifier = Modifier.fillMaxSize(),
                 cameraPositionState = cameraPositionState
             ) {<!-- -->
                 Clustering(items = parkMarkers,
                 onClusterClick = {<!-- -->
                     // Handle when the cluster is clicked
                     false
                 }, onClusterItemClick = {<!-- --> marker ->
                     // Handle when a marker in the cluster is clicked
                     selectedMarker = marker
                     false
                 })
             }
         }
     
         parkMarkers.forEach {<!-- -->
             Circle(
                 center = it.position,
                 radius = 120.0,
                 fillColor = Color.Green,
                 strokeColor = Color.Green
             )
         }

As shown above, set a circle for each Marker: specify the Marker’s position as the center and radius, and optionally the border color and fill color. (Box is a stacked layout, so that each circle can be displayed above the Map)

10. Display scale

A good map is usually accompanied by legends and diagrams to show how much physical distance a certain spatial scale on the map corresponds to. This helps users understand exactly how much space is represented in the map, since not every map is measured the same way.

But for those digital maps that support zooming, this requirement will increase the complexity of implementation because the displayed distance changes dynamically. Fortunately, Google Maps for Compose also takes this into consideration.

After importing the Widgets library, developers can use the two composable functions DisappearingScaleBar and ScaleBar. They are UI components at the top of the map interface that show the user a distance reference that changes in real time based on the zoom level.

 Box(contentAlignment = Alignment.Center) {<!-- -->
             GoogleMap(
                 modifier = Modifier.fillMaxSize(),
                 cameraPositionState = cameraPositionState
             ) {<!-- -->
                 // You can also use ScaleBar
                 DisappearingScaleBar(
                     modifier = Modifier
                     .padding(top = 5.dp, end = 15.dp)
                     .align(Alignment.TopStart),
                     cameraPositionState = cameraPositionState
                 )
     
                 Clustering(items = parkMarkers,
                 onClusterClick = {<!-- -->
                     // Handle when the cluster is clicked
                     false
                 }, onClusterItemClick = {<!-- --> marker ->
                     // Handle when a marker in the cluster is clicked
                     selectedMarker = marker
                     false
                 })
             }
         }
     
         parkMarkers.forEach {<!-- -->
             Circle(
                 center = it.position,
                 radius = 120.0,
                 fillColor = Color.Green,
                 strokeColor = Color.Green
             )
         }

As shown below, a scale bar will appear at the top of the Map, which will keep refreshing with the zoom level.

MAP study materials

Google Maps for Compose is an excellent way to integrate map functions in Compose, and there is a lot of other knowledge that requires continuous learning. If necessary, you can refer to the following information:

  • Google Maps for Compose Repo: The official Compose Map lib source code library. It also includes code examples, as well as instructions for reporting bugs, contributing code, etc.
  • Google Maps website for Android: Concepts and logic behind Google Map. Although it has nothing to do with the Compose library, it is actually the data support they provide behind the scenes, so it is necessary to understand it.
  • Google Maps Platform Discord Google Map’s official Discord service. Bring everyone together to discuss Google Map performance on multiple platforms, seek and provide help, and showcase individual integrated Map products.