Añadir y eliminar marcadores en un mapa con Flutter


Avatar de Pedro Cortez

Añadir marcadores a tus mapas puede ser una función indispensable para tu aplicación, ya sea para indicar un lugar al usuario o permitirle guardar un sitio. En esta guía, te explico cómo implementarlos.


Añadir y eliminar marcadores

Añadir y eliminar marcadores en un mapa de Google Maps

Ahora que has añadido un mapa de Google Maps a tu aplicación Flutter, aquí te mostramos cómo añadir marcadores para indicar puntos importantes o lugares guardados por el usuario:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MapScreen(),
    );
  }
}

class MapScreen extends StatefulWidget {
  @override
  _MapScreenState createState() => _MapScreenState();
}

class _MapScreenState extends State<MapScreen> {
  late GoogleMapController _mapController;
  Set<Marker> _markers = {};
  final FirebaseFirestore _firestore = FirebaseFirestore.instance;

  @override
  void initState() {
    super.initState();
    _loadMarkers();
  }

  // 🔥 CARGAR LOS MARCADORES DESDE FIRESTORE
  void _loadMarkers() async {
    var snapshot = await _firestore.collection('markers').get();
    setState(() {
      _markers = snapshot.docs.map((doc) {
        var data = doc.data();
        return Marker(
          markerId: MarkerId(doc.id),
          position: LatLng(data['lat'], data['lng']),
          infoWindow: InfoWindow(title: data['title'] ?? 'Marcador'),
          onTap: () => _showMarkerDialog(doc.id, data['title'] ?? "Sin título"), // 🟢 Mostrar popup
        );
      }).toSet();
    });
  }

  // 🎯 AÑADIR UN MARCADOR AL HACER CLIC EN EL MAPA
  void _addMarker(LatLng position) async {
    String markerId = DateTime.now().millisecondsSinceEpoch.toString();

    Marker newMarker = Marker(
      markerId: MarkerId(markerId),
      position: position,
      infoWindow: InfoWindow(title: 'Nuevo marcador'),
      onTap: () => _showMarkerDialog(markerId, 'Nuevo marcador'), // 🟢 Mostrar popup
    );

    setState(() {
      _markers.add(newMarker);
    });

    // 🔥 Guardar en Firestore
    await _firestore.collection('markers').doc(markerId).set({
      'lat': position.latitude,
      'lng': position.longitude,
      'title': 'Nuevo marcador'
    });
  }

  // 📌 MOSTRAR CUADRO DE INFORMACIÓN + ELIMINAR
  void _showMarkerDialog(String markerId, String title) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text("¿Qué deseas hacer?"),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text("Cerrar"),
          ),
          TextButton(
            onPressed: () {
              _deleteMarker(markerId);
              Navigator.pop(context);
            },
            child: Text("Eliminar", style: TextStyle(color: Colors.red)),
          ),
        ],
      ),
    );
  }

  // ❌ ELIMINAR UN MARCADOR
  void _deleteMarker(String markerId) async {
    setState(() {
      _markers.removeWhere((marker) => marker.markerId.value == markerId);
    });

    // 🔥 Eliminar de Firestore
    await _firestore.collection('markers').doc(markerId).delete();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Mapa con marcadores")),
      body: GoogleMap(
        initialCameraPosition: CameraPosition(
          target: LatLng(48.8566, 2.3522), // París como ejemplo
          zoom: 12,
        ),
        markers: _markers,
        onMapCreated: (controller) => _mapController = controller,
        onTap: (position) => _addMarker(position), // 🟢 Clic en el mapa => Añadir un marcador
      ),
    );
  }
}

En este ejemplo, el usuario puede añadir un marcador en el mapa con un simple clic. El lugar se guarda en Firestore para poder visualizarse en tiempo real. Al hacer clic en un marcador, se abre una ventana emergente con el título del marcador y un botón de «eliminar» que quita el marcador de Firestore, y por ende, del mapa.

Mostrar marcadores en un mapa de Google

Una vez añadido tu mapa de Google a tu aplicación, puedes agregarle marcadores utilizando la propiedad markers, de la siguiente forma:

GoogleMap(
  initialCameraPosition: CameraPosition(
    target: LatLng(48.8566, 2.3522),  // Posición de París
    zoom: 12,  // Nivel de zoom
  ),
  markers: _markers,  // Marcadores a mostrar en el mapa
  onMapCreated: (controller) => _mapController = controller,  // Inicializar el controlador del mapa
  onTap: (position) => _addMarker(position),  // Añadir un marcador al hacer clic en el mapa
)

Aquí, la variable _markers corresponde a la lista de los marcadores a mostrar, que es de tipo Set<Marker>. En mi ejemplo, los recupero desde una colección de Firestore, de la siguiente manera:

Set<Marker> _markers = {};
final FirebaseFirestore _firestore = FirebaseFirestore.instance;

@override
void initState() {
  super.initState();
  _loadMarkers();
}

// 🔥 CARGAR LOS MARCADORES DESDE FIRESTORE
void _loadMarkers() async {
  var snapshot = await _firestore.collection('markers').get();
  setState(() {
    _markers = snapshot.docs.map((doc) {
      var data = doc.data();
      return Marker(
        markerId: MarkerId(doc.id),
        position: LatLng(data['lat'], data['lng']),
        infoWindow: InfoWindow(title: data['title'] ?? 'Marcador'),
        onTap: () => _showMarkerDialog(doc.id, data['title'] ?? "Sin título"), // 🟢 Mostrar popup
      );
    }).toSet();
  });
}

¿Cómo se obtiene la posición en el mapa?

Cuando el usuario hace clic en un lugar del mapa, la función onTap del widget GoogleMap se llama automáticamente. Esta función recibe como parámetro la posición del clic en forma de un objeto LatLng, que es una clase definida por la API de Google Maps. Es este objeto LatLng el que contiene las coordenadas geográficas del punto clicado, es decir, la latitud y longitud.

Esto es lo que sucede exactamente:

1. Widget GoogleMap: En el widget GoogleMap, definimos un controlador de eventos para la función onTap, de la siguiente manera:

onTap: (position) => _addMarker(position),

Esta línea de código significa que cada vez que el usuario haga clic en algún lugar del mapa, la función onTap recuperará las coordenadas del lugar donde hizo clic el usuario y las pasará en forma de un objeto LatLng a la función _addMarker.

2. LatLng: El objeto position es una instancia de la clase LatLng, que contiene dos propiedades importantes:

  • latitude: La latitud del lugar donde el usuario hizo clic.
  • longitude: La longitud del lugar donde el usuario hizo clic.

Por ejemplo, si el usuario hace clic en un lugar determinado del mapa, position.latitude y position.longitude contendrán los valores exactos de latitud y longitud correspondientes a ese lugar específico.

3. Pasar la posición a la función _addMarker: Una vez que se recupera la posición en forma de objeto LatLng, esta se pasa a la función _addMarker. Esta función utiliza las coordenadas de latitud y longitud para colocar el marcador en el lugar exacto donde el usuario hizo clic en el mapa.

Crear un marcador al hacer clic en el mapa

Luego, vamos a permitir que un usuario añada un marcador en el mapa, en el lugar donde haga clic, y lo guarde en Firestore:

// Función que se coloca en la propiedad onTap del mapa

void _addMarker(LatLng position) async {
  String markerId = DateTime.now().millisecondsSinceEpoch.toString();  // Generación de un ID único
  Marker newMarker = Marker(
    markerId: MarkerId(markerId),
    position: position,  // Posición del marcador
    infoWindow: InfoWindow(title: 'Nuevo marcador'),  // Título del marcador
    onTap: () => _showMarkerDialog(markerId, 'Nuevo marcador'),  // Acción al hacer clic en el marcador
  );

  // Guardar el marcador en Firestore para hacerlo persistente
  await _firestore.collection('markers').doc(markerId).set({
    'lat': position.latitude,
    'lng': position.longitude,
    'title': 'Nuevo marcador',
  });
  
  setState(() {
    _markers.add(newMarker);  // Añadir el marcador a la lista de marcadores
  });
}

Aquí, añado los marcadores al hacer clic en el mapa, pero también es posible hacerlo de otra manera. Por ejemplo, con una barra de búsqueda, recuperando las coordenadas de un lugar y añadiéndolas a Firestore. El marcador aparecerá de forma dinámica en el mapa.

Explicación:

  • _addMarker(LatLng position): Esta función se llama cuando el usuario hace clic en el mapa.
  • markerId: Cada marcador tiene un identificador único. Aquí, utilizamos DateTime.now().millisecondsSinceEpoch.toString() para generar un ID único.
  • Marker: El marcador se crea con su posición, un título y una acción (onTap) que muestra un cuadro de diálogo cuando el usuario hace clic en él.
  • setState: Este método permite actualizar la interfaz de usuario para mostrar el nuevo marcador en el mapa.

Mostrar la información del marcador

En este ejemplo, quiero mostrar la información del marcador en una ventana emergente cuando el usuario hace clic en él:

// Función que se coloca en la propiedad onTap del marcador

void _showMarkerDialog(String markerId, String title) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: Text(title),
      content: Text("¿Qué deseas hacer?"),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),  // Cerrar la ventana emergente
          child: Text("Cerrar"),
        ),
        TextButton(
          onPressed: () {
            _deleteMarker(markerId);  // Eliminar el marcador
            Navigator.pop(context);
          },
          child: Text("Eliminar", style: TextStyle(color: Colors.red)),  // Eliminar el marcador
        ),
      ],
    ),
  );
}
  • showDialog: Esta función de Flutter muestra una ventana emergente.
  • AlertDialog: El widget utilizado para crear una ventana emergente con un título, contenido y acciones.
  • onPressed: Las acciones «Cerrar» y «Eliminar» permiten cerrar la ventana o eliminar el marcador.

Eliminar un marcador

Para eliminar un marcador del mapa y de la base de datos de Firestore, usamos una función dedicada. Así es como funciona:

void _deleteMarker(String markerId) async {
  setState(() {
    _markers.removeWhere((marker) => marker.markerId.value == markerId);  // Eliminar el marcador de la interfaz
  });

  // Eliminar el marcador de Firestore
  await _firestore.collection('markers').doc(markerId).delete();
}

La función _deleteMarker elimina un marcador en dos pasos:

  1. Se elimina de la lista _markers (que está vinculada a la interfaz de usuario).
  2. Se elimina de Firestore, asegurando que el marcador se elimina de la base de datos.

Personalizar los marcadores

Aquí hay algunas maneras de personalizar tus marcadores.

Cambiar el color de un marcador

Es posible modificar el color de los marcadores con colores predefinidos:

Marker(
  markerId: MarkerId("id-1"),
  position: LatLng(48.8566, 2.3522),
  icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueBlue),  // Marcador azul
)

Colores disponibles:

  • BitmapDescriptor.hueRed: Rojo
  • BitmapDescriptor.hueGreen: Verde
  • BitmapDescriptor.hueBlue: Azul
  • BitmapDescriptor.hueYellow: Amarillo

Usar un icono personalizado

También puedes usar una imagen personalizada como icono para el marcador:

Marker(
  markerId: MarkerId("custom-marker"),
  position: LatLng(48.8566, 2.3522),
  icon: BitmapDescriptor.fromAssetImage(
    ImageConfiguration(size: Size(48, 48)),
    'assets/custom_marker.png',  // Imagen personalizada
  ),
)

No olvides colocar tus imágenes en una carpeta de assets y declararlas en el archivo pubspec.yaml.

Modificar el tamaño del icono

Finalmente, también puedes modificar el tamaño del icono personalizado para adaptarlo a tus necesidades:

BitmapDescriptor.fromAssetImage(
  ImageConfiguration(devicePixelRatio: 2.5),  // Ajustar el tamaño
  'assets/custom_marker.png',
)
Avatar de Pedro Cortez