Barre de recherche Google Maps dans une application Flutter


Avatar de Pierre Courtois

Vous souhaitez afficher une barre de recherche google maps à votre application Flutter, pour afficher des lieux sur une carte, ou des informations ? Dans ce guide, je vous explique comment mettre tout cela en place.


Barre de recherche google maps dans Flutter

Implémentation de Google Maps

Si ce n’est pas encore fait, vous aurez besoin d’intégrer Google Maps à votre application Flutter, pour pouvoir utiliser ses services. Vous aurez donc besoin d’installer quelques packages, générer une clé API Google avec les bonnes autorisations et accorder certaines autorisations pour Android et IOS.

Installer les packages Google Maps

La première chose à fair est d’installer quelques packages essentiels qui vous permettront d’intégrer Google Maps et la fonctionnalité de recherche dans votre application Flutter.

Ajoutez ces packages à votre fichier pubspec.yaml :

#Remplacer par les dernières versions

dependencies:
  flutter:
    sdk: flutter
  google_maps_flutter: ^2.10.0
  google_places_flutter: ^2.1.0
  uuid: ^4.5.1
  http: ^1.3.0

Les packages utilisés ici sont :

  • google_maps_flutter : Pour intégrer une carte Google Maps dans votre application.
  • google_places_flutter : Pour l’auto-complétion des recherches de lieux.
  • uuid : Pour générer un identifiant unique pour les marqueurs de la carte.
  • http : Pour effectuer des requêtes HTTP pour récupérer les détails d’un lieu.

Permissions nécessaires pour votre clé API Google

Pour utiliser les services Google Maps et Places, vous devez activer les API Google appropriées et obtenir une clé API. Suivez ces étapes :

  1. Allez sur la Console Google Cloud.
  2. Créez un projet (si ce n’est pas déjà fait).
  3. Activez les API suivantes sur une clé déjà existante, ou une que vous venez de créer :
    • Google Maps SDK for Android
    • Google Maps SDK for iOS
    • Places API
    • Geocoding API
    • Directions API

Permissions Android et iOS

Si vous ne l’avez pas encore fait, pensez également à ajouter les permissions suivantes dans le fichier AndroidManifest.xml de votre application Android (situé sous android/app/src/main/AndroidManifest.xml).

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>

De plus, dans la même section, vous devez aussi configurer la clé API Google pour Android :

<meta-data
            android:name="com.google.android.geo.API_KEY"
            android:value="VOTRE_CLÉ_API" />

Vous pouvez placer ce bloc juste avant le </application> qui vient fermer le manifest

Pour iOS, vous devez définir la clé API dans le fichier AppDelegate.swift. Ajoutez ce code à l’intérieur de la méthode didFinishLaunchingWithOptions.

GMSServices.provideAPIKey("YOUR_GOOGLE_API_KEY")

Enfin, dans le fichier Info.plist (situé sous ios/Runner/Info.plist), ajoutez :

<key>NSLocationWhenInUseUsageDescription</key>
<string>Nous avons besoin de votre position pour afficher des lieux autour de vous.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Nous avons besoin de votre position pour afficher des lieux autour de vous.</string>

Implémentation complète de la barre de recherche

Voici un exemple de code complet qui intègre la barre de recherche Google Maps dans une application Flutter. Ce code utilise les API Google Maps et Places pour permettre à un utilisateur de rechercher un lieu, afficher un marqueur sur la carte, et déplacer la caméra vers la position du lieu sélectionné. Vous pouvez facilement le réutiliser selon vos besoins, par exemple pour afficher les informations d’un lieu sélectionné, plutôt que de le montrer sur une carte.

import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:google_places_flutter/google_places_flutter.dart';
import 'package:google_places_flutter/model/prediction.dart';
import 'package:uuid/uuid.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Recherche Google Maps',
      home: const MapScreen(),
    );
  }
}

class MapScreen extends StatefulWidget {
  const MapScreen({Key? key}) : super(key: key);

  @override
  _MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController mapController;
  Set<Marker> _markers = {};
  final String googleApiKey = "YOUR_GOOGLE_API_KEY";
  final uuid = Uuid();
  TextEditingController _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("Recherche d'endroits")),
      body: Stack(
        children: [
          GoogleMap(
            onMapCreated: (controller) => mapController = controller,
            initialCameraPosition: const CameraPosition(
              target: LatLng(48.8566, 2.3522),
              zoom: 12.0,
            ),
            markers: _markers,
          ),
          Positioned(
            top: 10,
            left: 15,
            right: 15,
            child: Padding(
              padding: const EdgeInsets.all(10.0),
              child: GooglePlaceAutoCompleteTextField(
                textEditingController: _controller,
                googleAPIKey: googleApiKey,
                inputDecoration: InputDecoration(
                  hintText: "Rechercher un endroit...",
                  border: OutlineInputBorder(),
                  filled: true,
                  fillColor: Colors.white,
                ),
                debounceTime: 800,
                countries: ["fr"],
                isLatLngRequired: true,
                getPlaceDetailWithLatLng: (Prediction prediction) async {
                  await getPlaceDetailsFromPlaceId(prediction);
                },
                itemClick: (Prediction prediction) {
                  _controller.text = prediction.description ?? "";
                  _controller.selection = TextSelection.fromPosition(
                      TextPosition(offset: _controller.text.length));
                  _addMarkerAndMoveCamera(prediction);
                },
              ),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> getPlaceDetailsFromPlaceId(Prediction prediction) async {
    final String apiKey = "YOUR_GOOGLE_API_KEY";
    if (prediction.placeId == null) return;

    final String url =
        "https://maps.googleapis.com/maps/api/place/details/json?place_id=${prediction.placeId}&key=$apiKey";

    try {
      final response = await http.get(Uri.parse(url));
      if (response.statusCode == 200) {
        final Map<String, dynamic> data = json.decode(response.body);
        if (data['status'] == "OK") {
          double lat = data['result']['geometry']['location']['lat'];
          double lng = data['result']['geometry']['location']['lng'];
          _addMarkerAndMoveCamera(prediction, lat, lng);
        }
      }
    } catch (e) {
      print("Erreur lors de la récupération des détails du lieu : $e");
    }
  }

  void _addMarkerAndMoveCamera(Prediction prediction, [double? lat, double? lng]) {
    if (lat == null || lng == null) return;

    final markerId = uuid.v4();
    final marker = Marker(
      markerId: MarkerId(markerId),
      position: LatLng(lat, lng),
      infoWindow: InfoWindow(
        title: prediction.description ?? "Lieu",
      ),
    );

    setState(() {
      _markers.clear();
      _markers.add(marker);
    });

    mapController.animateCamera(CameraUpdate.newLatLng(LatLng(lat, lng)));
  }
}

Explication du code et des étapes principales

Maintenant, je vais rapidement vous expliquer les différentes fonctionnalités de ce code.

La barre de recherche

La barre de recherche est gérée par le widget GooglePlaceAutoCompleteTextField. Ce widget est une interface utilisateur fournie par le package google_places_flutter pour interagir avec l’API Google Places, et il permet d’implémenter la fonctionnalité d’auto-complétion de recherche.

Voici le code qui initialise la barre de recherche :

GooglePlaceAutoCompleteTextField(
  textEditingController: _controller,
  googleAPIKey: googleApiKey,
  inputDecoration: InputDecoration(
    hintText: "Rechercher un endroit...",
    border: OutlineInputBorder(),
    filled: true,
    fillColor: Colors.white,
  ),
  debounceTime: 800,
  countries: ["fr"],
  isLatLngRequired: true,
  getPlaceDetailWithLatLng: (Prediction prediction) async {
    await getPlaceDetailsFromPlaceId(prediction);
  },
  itemClick: (Prediction prediction) {
    _controller.text = prediction.description ?? "";
    _controller.selection = TextSelection.fromPosition(
        TextPosition(offset: _controller.text.length));
    _addMarkerAndMoveCamera(prediction);
  },
)

Voici à quoi servent les différentes propriétés :

  • textEditingController: _controller : Contrôleur qui contient la valeur affichée dans le champ de texte.
  • googleAPIKey: googleApiKey : La clé API que vous avez générée et qui est nécessaire pour interagir avec les API Google. Elle permet à Google d’identifier votre application et de vous donner accès à ses services de manière sécurisée services.
  • inputDecoration: InputDecoration(...) : C’est grâce à cette propriété que vous allez pouvoir personnaliser l’apparence du champ de recherche. Vous pouvez notammenet modifier la couleur de fond, le texte d’indication (hintText), et l’outline (bordure) du champ.
  • debounceTime: 800 : Ce paramètre définit le délai d’attente en millisecondes avant de lancer la recherche après que l’utilisateur ait arrêté de taper. Cela évite d’envoyer une requête à chaque caractère tapé, réduisant ainsi le nombre de requêtes envoyées à Google.
  • countries: ["fr"] : Cela limite les résultats de la recherche aux pays spécifiés. Ici, on limite la recherche à la France ("fr").
  • isLatLngRequired: true : Cela demande à l’API Places de retourner la latitude et la longitude de chaque lieu trouvé. Cela est nécessaire pour pouvoir placer un marqueur sur la carte.
  • getPlaceDetailWithLatLng : C’est un callback qui sera exécuté lorsque l’utilisateur choisira un lieu. Ce callback prend un objet Prediction qui contient les informations de base sur le lieu sélectionné, comme son ID et sa description.
  • itemClick : Ce callback est appelé lorsque l’utilisateur clique sur un lieu dans la liste des suggestions. Lorsque cela se produit, la barre de recherche est mise à jour avec la description du lieu sélectionné et un marqueur est ajouté sur la carte à l’endroit du lieu.

La sélection d’un lieu

Lorsque l’utilisateur sélectionne un lieu dans la liste des prédictions, l’événement itemClick est déclenché :

itemClick: (Prediction prediction) {
  // Mettre à jour le texte de la barre de recherche avec la description du lieu
  _controller.text = prediction.description ?? "";
  _controller.selection = TextSelection.fromPosition(
    TextPosition(offset: _controller.text.length));

  // Ajouter un marqueur et déplacer la caméra vers le lieu
  _addMarkerAndMoveCamera(prediction);
},

Voilà ce qui se passe :

  • _controller.text = prediction.description ?? "" : Lorsque l’utilisateur sélectionne un lieu, cette ligne met à jour le texte dans la barre de recherche avec la description du lieu qui a été selectionné. Par exemple, si l’utilisateur a cliqué sur « Tour Eiffel », la description qui apparaîtra dans la barre sera « Tour Eiffel ».
  • _controller.selection = TextSelection.fromPosition(TextPosition(offset: _controller.text.length)) : Cela permet de déplacer le curseur du texte à la fin de la description du lieu, afin que l’utilisateur puisse voir immédiatement la description complète dans la barre de recherche.
  • _addMarkerAndMoveCamera(prediction) : Cette ligne appelle une fonction qui va ajouter un marqueur à l’endroit sélectionné sur la carte et déplacer la caméra vers ce lieu.

Récupérer les détails d’un lieu

Une fois qu’un lieu est sélectionné, il est possible de récupérer plus d’informations sur celui-ci, comme sa latitude, sa longitude, son adresse, etc. Pour ce faire, vous pouvez appeler la fonction getPlaceDetailsFromPlaceId, qui envoie une requête à l’API Google Places pour obtenir les détails d’un lieu en utilisant son placeId.

Future<void> getPlaceDetailsFromPlaceId(Prediction prediction) async {
  final String apiKey = "YOUR_GOOGLE_API_KEY";
  
  if (prediction.placeId == null) return;

  final String url = "https://maps.googleapis.com/maps/api/place/details/json?place_id=${prediction.placeId}&key=$apiKey";

  try {
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      final Map<String, dynamic> data = json.decode(response.body);
      if (data['status'] == "OK") {
        double lat = data['result']['geometry']['location']['lat'];
        double lng = data['result']['geometry']['location']['lng'];

        _addMarkerAndMoveCamera(prediction, lat, lng);
      }
    }
  } catch (e) {
    print("Erreur lors de la récupération des détails du lieu : $e");
  }
}

Voilà les étapes qui se déroulent ici :

  • if (prediction.placeId == null) return; : Cette ligne vérifie si l’ID du lieu est disponible. Si ce n’est pas le cas, la fonction se termine prématurément. En effet, un placeId est nécessaire pour obtenir des détails supplémentaires sur un lieu.
  • final String url = "https://maps.googleapis.com/maps/api/place/details/json?place_id=${prediction.placeId}&key=$apiKey"; : Cette ligne construit l’URL de la requête à envoyer à l’API Google Places. Nous incluons l’ID du lieu et notre apiKey pour authentifier la requête. Celle-ci echouera si un des deux éléments est manquant.
  • final response = await http.get(Uri.parse(url)); : La fonction http.get envoie une requête GET à l’API Google Places et récupère une réponse.
  • if (response.statusCode == 200) : Si la réponse a un code de statut 200 (ce qui signifie que la requête a réussi), nous traitons les données retournées.
  • final Map<String, dynamic> data = json.decode(response.body); : Nous décodons la réponse JSON en un objet Dart pour pouvoir l’exploiter.
  • double lat = data['result']['geometry']['location']['lat']; et double lng = data['result']['geometry']['location']['lng']; : Nous extrayons la latitude et la longitude du lieu à partir des données retournées par l’API.
  • _addMarkerAndMoveCamera(prediction, lat, lng); : Après avoir récupéré les coordonnées géographiques du lieu, cette fonction est appelée pour placer un marqueur sur la carte et déplacer la caméra à cet emplacement.

Le placeId est un identifiant unique attribué par l’API Google Places à chaque lieu ou établissement répertorié dans sa base de données. Il permet de récupérer des informations détaillées sur un lieu spécifique, comme son adresse, ses coordonnées géographiques, et d’autres données associées.

Placer un marqueur sur la carte et déplacer la caméra

Enfin, la fonction _addMarkerAndMoveCamera prend un Prediction et les coordonnées géographiques du lieu, puis ajoute un marqueur sur la carte et déplace la caméra :

void _addMarkerAndMoveCamera(Prediction prediction, [double? lat, double? lng]) {
  if (lat == null || lng == null) return;

  final markerId = uuid.v4();
  final marker = Marker(
    markerId: MarkerId(markerId),
    position: LatLng(lat, lng),
    infoWindow: InfoWindow(
      title: prediction.description ?? "Lieu",
    ),
  );

  setState(() {
    _markers.clear();
    _markers.add(marker);
  });

  mapController.animateCamera(CameraUpdate.newLatLng(LatLng(lat, lng)));
}

Voilà ce que fait la fonction :

  • if (lat == null || lng == null) return; : Si les coordonnées sont manquantes (null), la fonction s’arrête. Cela garantit qu’il n’y a pas de tentative d’ajouter un marqueur sans position valide.
  • final markerId = uuid.v4(); : On génére un identifiant unique pour chaque marqueur à l’aide de la bibliothèque uuid.
  • final marker = Marker(...) : Ensuite, on créer un objet Marker qui contient la position (latitude et longitude) et l’information qui apparaîtra dans la fenêtre d’information du marqueur (ici, la description du lieu).
  • setState(() {...}) : Cette méthode permet de mettre à jour l’état de l’application et de redessiner la carte avec le nouveau marqueur ajouté.
  • mapController.animateCamera(CameraUpdate.newLatLng(LatLng(lat, lng))); : Enfin, cette ligne déplace la caméra vers les nouvelles coordonnées du lieu, en centrant la carte sur l’emplacement du marqueur.

Personnaliser la barre de recherche

La barre de recherche de Google Places peut être facilement personnalisée en modifiant ses propriétés visuelles. Par exemple, vous pouvez changer la couleur de fond, la couleur du texte, ou l’apparence des bordures. Voici quelques options de personnalisation courantes :

Changer la couleur de fond

Pour que la barre de recherche ait un fond blanc, vous pouvez définir la propriété fillColor :

fillColor: Colors.white

Changer la couleur du texte

Vous pouvez modifier la couleur du texte avec la propriété textStyle. Par exemple, pour avoir le texte en noir :

textStyle: TextStyle(color: Colors.black)

Ajouter un bord arrondi

Pour que la barre de recherche ait des bords arrondis, utilisez la propriété borderRadius comme suit :

borderRadius: BorderRadius.circular(8.0)

Ajouter des pays à la recherche

Google Places permet de restreindre la recherche à un ou plusieurs pays en spécifiant leurs codes de pays dans la propriété countries de GooglePlaceAutoCompleteTextField. Cela permet d’afficher des résultats uniquement pour des lieux dans les pays spécifiés. Par exemple, pour limiter la recherche à la France et aux États-Unis, vous pouvez ajouter leurs codes respectifs :

countries: ["fr", "us"]

Les codes de pays sont des abréviations en deux lettres définies par la norme ISO 3166-1 alpha-2. Voici quelques codes pour des pays populaires :

  • France : "fr"
  • États-Unis : "us"
  • Royaume-Uni : "gb"
  • Canada : "ca"
  • Allemagne : "de"

Personnalisation des fonctionnalités

Vous pouvez aussi personnaliser le comportement de la barre de recherche en jouant avec des paramètres comme le délai de debounce et les callbacks.

Délai de debounce

Le debounce permet de réduire la fréquence des requêtes envoyées à l’API pendant que l’utilisateur tape. Cela évite de surcharger le serveur avec des requêtes inutiles pour chaque touche pressée. Le paramètre debounceTime dans GooglePlaceAutoCompleteTextField définit ce délai. Par exemple :

debounceTime: 800 // 800ms avant d'envoyer la requête après que l'utilisateur ait cessé de taper.

Callback sur la soumission

Vous pouvez définir un callback qui sera exécuté lorsqu’un utilisateur soumet la recherche (en appuyant sur « Entrée » ou en sélectionnant un lieu). Cela vous permet de déclencher une action personnalisée après la soumission. Par exemple :

formSubmitCallback: () {
  // Action personnalisée après soumission
}

Récupérer les informations du lieu sélectionné grâce à l’API Google Places

Enfin, l’API Google Places fournit des informations détaillées sur un lieu à partir de son placeId. Ces informations incluent :

1. Les coordonnées géographiques : L’API vous renvoie les coordonnées exactes (latitude et longitude) du lieu. Ces données sont particulièrement utiles pour centrer la carte ou pour d’autres usages géographiques.

double lat = data['result']['geometry']['location']['lat'];
double lng = data['result']['geometry']['location']['lng'];

2. Le nom du lieu : Le nom complet du lieu, comme il apparaît sur Google. Par exemple, cela peut être le nom d’un restaurant, d’un magasin ou d’un monument.

String name = data['result']['name']; 

3. L’adresse complète : L’adresse complète du lieu, comme la rue, la ville, le code postal, etc. Cela peut être récupéré sous la forme d’un champ formatted_address.

String address = data['result']['formatted_address'];

4. Numéro de téléphone : Si disponible, l’API peut vous fournir le numéro de téléphone du lieu sous la propriété formatted_phone_number.

String phone = data['result']['formatted_phone_number'];

5 Les avis sur le lieux : L’API vous fournit la note moyenne du lieu ainsi que les avis laissés par les utilisateurs. Par exemple, vous pouvez récupérer la note avec le champ rating.

double rating = data['result']['rating'];

L’API renvoie ces données sous forme de JSON, que vous pouvez ensuite traiter et afficher selon vos besoins dans l’application.

Avatar de Pierre Courtois