Cet article est basé sur une conférence donnée lors de Google I/O Extended pour Google Developers Group à Londres en juillet 2023. Les diapositives de la conférence sont disponibles ici.
Il est difficile d'imaginer que Jetpack Compose 1.0 soit sorti en juillet 2021 . Deux ans plus tard, avec 24 % des 1 000 meilleures applications de Google Play ayant adopté Compose, il est facile de comprendre pourquoi.
Parmi toute cette excitation, un aspect du développement Android moderne qui, à mon avis, reçoit peu d'attention est Google Maps. Cela fait un moment que je n'ai pas utilisé le SDK, j'ai donc été agréablement surpris de voir que Google Maps rattrapait son retard et publiait sa propre bibliothèque Compose .
Ce sera une bonne nouvelle pour les entreprises et les ingénieurs travaillant dans le domaine de la cartographie. La cartographie mobile est une industrie de 35,5 milliards de dollars, et on prévoit qu'elle atteindra 87,7 milliards de dollars d'ici 2028 avec un taux de croissance annuel composé (TCAC) de 19,83 %. Source
Pourquoi est-ce important? Un marché plus vaste signifie davantage de possibilités pour les entreprises de tirer des revenus des applications de cartographie mobile. Ceux-ci vont des cas d’utilisation habituels, aux services de nourriture, de livraison d’épicerie et de covoiturage. Cependant, si l’on creuse profondément, il existe des applications qui ne sont pas immédiatement évidentes.
Voici les exemples que j’ai pu trouver après une brève recherche :
Les cartes mobiles sont idéales pour les villes intelligentes, car elles aident à gérer le rythme cardiaque d'une ville et à visualiser les données de manière à mieux comprendre et réagir à ses défis. Utile pour les urbanistes, les organismes d’intervention d’urgence ou les résidents ordinaires.
La gestion des ressources bénéficie également des solutions de cartographie. Allant de l'agriculture à la pêche, de l'exploitation minière à la foresterie, les cartes fournissent aux personnes travaillant dans ce secteur une perspective pour prendre les bonnes décisions pour récolter les matériaux de manière durable.
Les transports dépendent fortement de la technologie de cartographie. Pas seulement des applications grand public comme Google Maps ou Uber, mais aussi des fonctions commerciales telles que comprendre où se trouve la flotte de véhicules d'une entreprise. Les agences de transport utilisent également des cartes pour gérer le trafic et aider à prendre des décisions sur l'endroit où diriger le trafic afin de faciliter la circulation.
Enfin, le changement climatique et la météo étant de plus en plus imprévisibles, les cartes permettent aux agences météorologiques, aux unités d'intervention d'urgence et aux défenseurs de la faune de comprendre comment notre monde évolue et ce que nous pouvons faire pour prendre des mesures positives pour réduire cela.
Sources :Mordor Intelligence , GMInsights , Allied Market Research , EMR Research , Google Earth Outreach , Research & Markets
Alors que le monde fournit de plus en plus de données, c'est le bon moment pour apprendre à mettre ces données sur une carte. Faisons cela et revenons au code.
Google Maps for Compose s'appuie sur les dépendances suivantes :
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 est construit sur le SDK Google Maps, vous devez donc importer la bibliothèque Compose et le SDK Maps. Vous n'aurez pas besoin d'utiliser la plupart des objets du SDK Google Maps, car la bibliothèque de composition en regroupe la plupart dans Composables.
Les bibliothèques d'utilitaires et de widgets sont une dépendance facultative. La bibliothèque d'utilitaires offre la possibilité de regrouper des marqueurs sur les cartes, tandis que les widgets fournissent des composants d'interface utilisateur supplémentaires. Vous les verrez utilisés plus tard.
Pour cet article, j'ai inclus la bibliothèque de demandes d'autorisations d'Accompanist pour montrer comment demander des autorisations de localisation, une autorisation souvent utilisée avec des cartes. Accompanist est une bibliothèque expérimentale permettant à Google d'essayer et de recueillir des commentaires sur les fonctionnalités qui ne font pas encore partie de Jetpack Compose.
Enfin, vous devez accéder à la console de développement Google , vous inscrire pour obtenir une clé API du SDK Google Maps et l'ajouter à votre projet. Il existe un guide sur la documentation du développeur Google Maps expliquant comment procéder.
Conseil de sécurité : dans la Google Developer Console, verrouillez votre clé API afin qu'elle ne fonctionne qu'avec votre application. Cela évite toute utilisation non autorisée.
Afficher une carte est aussi simple que ci-dessous :
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" ) } }
Créez un objet LatLng
avec la position d'une zone et utilisez-le conjointement avec rememberCameraPositionState
pour définir la position initiale de la caméra. Cette méthode mémorise la position de la carte lorsque vous vous déplacez avec vos mains ou par programme. Sans cette méthode, Compose recalculerait la carte à sa position initiale à chaque changement d'état.
Ensuite, créez une composition GoogleMap
et transmettez un modificateur de votre choix et l'état de la caméra. GoogleMap
fournit également une API Slot pour transmettre des composables supplémentaires, ces composables sont ce que vous souhaitez dessiner sur la carte.
Ajoutez un composable Marker
, puis ajoutez un MarkerState
contenant la position du marqueur à l'intérieur. Enfin, ajoutez un titre et une description du marqueur.
L'exécuter donne une belle vue aérienne de l'ouest de Londres avec un marqueur à Hyde Park.
Vous pouvez personnaliser la fenêtre du marqueur à l'aide d'un Composable MarkerInfoWindowContent
. Il dispose également d'une API basée sur les emplacements, ce qui signifie que vous pouvez transmettre vos composables pour afficher votre interface utilisateur personnalisée dans la fenêtre.
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" ) } } } }
L'exécution de ceci affiche la fenêtre personnalisée au-dessus du marqueur lorsque vous appuyez dessus.
Afficher plusieurs marqueurs est aussi simple que d’en ajouter autant que nécessaire. Ajoutons des marqueurs pour quelques parcs différents dans l'ouest de Londres.
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" ) } }
Exécutez le code et vous verrez vos marqueurs apparaître sur la carte.
Une carte peut être occupée en peu de temps. Si vous essayez d'afficher 300 marqueurs, il sera visuellement difficile pour un utilisateur de comprendre ce qui se passe. Google Maps et votre appareil ne vous remercieront pas non plus, car ils devront restituer chaque marqueur , ce qui aura un impact sur les performances et la durée de vie de la batterie.
La solution à ce problème est le Clustering , une technique regroupant des marqueurs proches les uns des autres en un seul marqueur. Ce clustering se produit au niveau du zoom. Lorsque vous effectuez un zoom arrière sur la carte, les marqueurs se regroupent en un cluster, tandis que lorsque vous effectuez un zoom avant, le cluster se divise en marqueurs individuels.
Google Maps for Compose fournit cela immédiatement via un composable Clustering
. Il n'est pas nécessaire d'écrire un tri ou un filtrage complexe pour que le clustering se produise.
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 }
Notez que la classe de données ParkItem
a été ajoutée. Nous en avons besoin car les éléments passés dans un composable Clustering
doivent être conformes à l'interface ClusterItem
. L'interface fournit au cluster la position, le titre et l'extrait de chaque marqueur.
Effectuez un zoom avant et arrière et vous verrez le clustering en action.
Les cartes et la position de l'utilisateur vont souvent de pair, il est donc logique que certaines applications de cartographie demandent l'autorisation de connaître la position de l'utilisateur.
Traitez l'autorisation d'un utilisateur avec respect si vous faites cela, l'autorisation de localisation est l'une des autorisations les plus sensibles à acquérir auprès d'un utilisateur.
Assurez-vous d'informer l'utilisateur de la raison pour laquelle vous avez besoin de cette autorisation et de montrer activement les avantages de l'accorder. Des points bonus si votre application fonctionne partiellement sans aucune autorisation.
Google propose d'excellents guides sur la façon de gérer la localisation des utilisateurs , ainsi qu'un guide séparé pour accéder aux données de localisation en arrière-plan .
Vous avez donc fait preuve de diligence raisonnable et décidé que vous aviez besoin de l'autorisation de l'utilisateur pour accéder à l'emplacement. Avec la bibliothèque de permissions dans Accompanist, procédez comme suit :
// 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") } } }
Via un accompagnateur, nous vérifions si l'application a accès à l'autorisation ACCESS_FINE_LOCATION
, ou à un haut niveau de précision GPS en anglais. Il est important d'inclure les autorisations demandées dans le manifeste Android, car vous ne pourrez pas demander les autorisations autrement.
Le système Android et Google Play Store utilisent également le manifeste pour comprendre le fonctionnement de votre application et informer les utilisateurs.
Si l'autorisation n'est pas accordée, une petite boîte de dialogue composable s'affiche expliquant la nécessité de l'autorisation et un bouton pour lancer la demande d'autorisation via le système.
Alors que la plupart des applications cartographiques nécessitent qu'un utilisateur déplace la carte au toucher, Google Maps for Compose fournit des API pour déplacer la carte par programme. Cela peut être utile si vous souhaitez accéder à une zone spécifique en réponse à un événement.
Dans cet exemple, nous allons naviguer doucement dans l'application à travers notre collection de marqueurs.
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 } } } }
L'élément clé ici est le code à l'intérieur du LaunchedEffect
. Pour chaque marqueur, l'application configure un appel cameraPositionState.animate()
pour accéder au marqueur. La caméra reçoit ces informations via une mise à jour de la caméra, créée à l'aide de CameraUpdateFactory.newLatLngZoom()
.
Cette méthode prend un LatLng
, un float indiquant le niveau de zoom de la carte et un long pour définir la durée de l'animation.
Enfin, pour espacer les animations nous utilisons delay()
pour ajouter une pause de 4 secondes entre chaque animation.
Ce n'est pas seulement une carte aérienne avec laquelle Google Maps for Compose vous aide. Vous pouvez également autoriser les applications à accéder à Street View , affichant une vue à 360° d'un emplacement. Pour ce faire, utilisez le composable StreetView
:
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 }) } } }
Dans cet exemple, nous définissons la variable selectedMarker
chaque fois qu'un marqueur est enfoncé. Si un marqueur est sélectionné, nous affichons Street View, en passant par la position du marqueur.
Vous souhaiterez peut-être dessiner vos propres formes et annotations sur la carte. Google Maps for Compose fournit un certain nombre de composables pour ce faire. Dans cet article, nous allons utiliser le composable Circle
.
Un cercle est une bonne forme à utiliser si votre application utilise des géofences pour réagir aux modifications apportées à l'emplacement d'un utilisateur. Un cercle peut représenter la zone dans laquelle une barrière géographique est 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 ) }
Ici, nous mettons en place un cercle pour chacun de nos marqueurs. Créer un cercle implique de lui transmettre une position et la taille du rayon du cercle. Nous utilisons également deux paramètres facultatifs pour définir la couleur de la bordure et remplir la couleur du cercle.
Une bonne carte est souvent accompagnée de légendes et de diagrammes montrant à quoi équivaut une mesure d’espace sur la carte en distance. Cela vous donne une idée des espaces impliqués dans la carte, car toutes les cartes n'utilisent pas la même forme de mesure.
Pour les cartes numériques pouvant effectuer un zoom avant et arrière, cela ajoute une couche particulière de complexité car les distances représentées peuvent changer de manière dynamique. Heureusement, Google Maps pour Compose est là pour vous.
À l'aide de la bibliothèque Widgets, vous accédez aux composables DisappearingScaleBar
et ScaleBar
. Il s'agit de composants d'interface utilisateur situés en haut de la carte, fournissant aux utilisateurs une mesure de distance qui change en fonction du niveau de zoom.
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 ) }
Après avoir ajouté le composable, vous obtenez un ScaleBar qui change en fonction du niveau de zoom en haut de la carte.
Google Maps for Compose est un excellent moyen de travailler avec Google Maps et il y a bien d'autres choses à apprendre. Voici quelques endroits que je recommande si vous avez besoin d’aide :