Flutter Flow: Google Map Custom Markers with Firebase

Flutter Flow: Google Map Custom Markers with Firebase

I have previously shown how to create Google Maps with custom Markers using local data and Supabase.
Flutter Flow: Adding Custom Markers for Google Maps
Flutter Flow: Use Supabase to show custom markers on Google Maps

I will show you how to create a Google Map with custom Markers using Firebase Firestore.

Custom development

I get a lot of requests for custom tweaks of Google Maps with Flutter.

I have created a form to better understand everyone’s needs. You can also request custom Flutter Flow development and/or Flutter Flow training.

https://coffeebytez.com/contact-me/

I will focus on the most highly requested items first, which will be available on my substack. I will also post exclusive in-depth tutorials there.

Please subscribe to my substack here:

https://coffeebytez.substack.com/?r=gdmkm&utm_campaign=pub-share-checklist

If you have urgent specific requests, please leave your contact information in the survey.

Set Up Firebase

If you haven’t already, follow Flutter Flows docs on setting up a new Firebase Project with Flutter Flow.
Firebase Setup | FlutterFlow Docs

Also, make sure to enable and configure your Firestore Database. These are different links, although the preview is the same.
Firebase Setup | FlutterFlow Docs

Add Data to your Firestore Database

Next, let’s add data to a Firestore Database. For this example, I am going to manually add data.

Create a collection called place, give it a unique Document ID, and add the following fields

  • title: This is the title that shows on the popup when clicking the marker. Give it the string data type
  • description: This is the description that shows on the popup when clicking the marker. Give it the string data type
  • latitude : This will hold our latitude for each marker. Give it the number data type
  • longitude : This will hold our longitude for each marker. Give it the number data type.
  • image_url: This will be our image name including the file extension. You can also specify an url for network images. Give it the string data type

Click Save

Create Firestore Schema in Flutter Flow

Next, in Flutter Flow, go to the Firestore menu on the far left.

Next, add a place schema with the same fields as our Firestore document in our Firestore place collection. The only difference here is we will use double as the data type for longitude and latitude.

Using Geopoint

This is optional, but probably the preferred way.

Firestore also has a geopoint field. So instead we can also set it in Firestore like that.

In the place schema, add another field as LatLng with the same name you used in Firestore. In this case, it’s geopoint.

Create a Custom Widget

Next we will create a new custom Widget in Flutter Flow.

Click the Custom Code menu

Click Add > Widget

Give the new widget a name such as Firestore Map

Now lets add some parameters. The first parameter is places, where we will pass a list of Firestore **Documents. **All of the other parameters are straight forward. For the double and bool parameters, make sure to uncheck Nullable

Now let’s add one lasts parameter, onClickMarker, which is an Action Type with an Action Parameter of Type Document. The Document Type is **place. **We will use this to save the Document to Page or Widget state whenever a Marker is clicked.

Now click the green Add Boilerplate button to add the default code for this Custom Widget.

Now let’s add the following dependencies.

Note: Adding dependencies is no longer required

google_maps_flutter: ^2.5.0
google_maps_flutter_web: ^0.5.2
google_maps_flutter_platform_interface: ^2.4.1

Click Save, then click the compile button (button with a hammer)

Add Custom Code

Now let’s add the Custom Code. It is almost the same as the Supabase Version.
Flutter Flow: Google Maps Custom Marker Actions

The only difference is the parameter types of PlaceRecord (Firestore Document) instead of Place Row (Supabase Row).

Get the full code here

// Automatic FlutterFlow imports
import '/backend/backend.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:google_maps_flutter/google_maps_flutter.dart'
    as google_maps_flutter;
import '/flutter_flow/lat_lng.dart' as latlng;
import 'dart:async';
export 'dart:async' show Completer;
export 'package:google_maps_flutter/google_maps_flutter.dart' hide LatLng;
export '/flutter_flow/lat_lng.dart' show LatLng;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'dart:ui';

class FirestoreMap extends StatefulWidget {
  const FirestoreMap({
    super.key,
    this.width,
    this.height,
    this.places,
    required this.centerLatitude,
    required this.centerLongitude,
    required this.showLocation,
    required this.showCompass,
    required this.showMapToolbar,
    required this.showTraffic,
    required this.allowZoom,
    required this.showZoomControls,
    required this.defaultZoom,
    this.onClickMarker,
  });

  final double? width;
  final double? height;
  final List<PlaceRecord>? places;
  final double centerLatitude;
  final double centerLongitude;
  final bool showLocation;
  final bool showCompass;
  final bool showMapToolbar;
  final bool showTraffic;
  final bool allowZoom;
  final bool showZoomControls;
  final double defaultZoom;
  final Future Function(PlaceRecord? placeRow)? onClickMarker;

  @override
  State<FirestoreMap> createState() => _FirestoreMapState();
}

class _FirestoreMapState extends State<FirestoreMap> {

  ...

   @override  
   void didUpdateWidget(FirestoreMap oldWidget) { 
       super.didUpdateWidget(oldWidget);  
       if (oldWidget.places != widget.places) {    
           _loadMarkerIcons();    
       }   
   }

  Future<void> _loadMarkerIcons() async {
    Set<String?> uniqueIconPaths =
        widget.places?.map((data) => data.imageUrl).toSet() ??
            {}; // Extract unique icon paths

    for (String? path in uniqueIconPaths) {
      if (path != null && path.isNotEmpty) {
        if (path.contains("https")) {
          Uint8List? imageData = await loadNetworkImage(path);
          if (imageData != null) {
            google_maps_flutter.BitmapDescriptor descriptor =
                await google_maps_flutter.BitmapDescriptor.fromBytes(imageData);
            _customIcons[path] = descriptor;
          }
        } else {
          google_maps_flutter.BitmapDescriptor descriptor =
              await google_maps_flutter.BitmapDescriptor.fromAssetImage(
            const ImageConfiguration(devicePixelRatio: 2.5),
            "assets/images/$path",
          );
          _customIcons[path] = descriptor;
        }
      }
    }

    _updateMarkers(); // Update markers once icons are loaded
  }

...

  Set<google_maps_flutter.Marker> _createMarkers() {
    var tmp = <google_maps_flutter.Marker>{};
    for (int i = 0; i < (widget.places ?? []).length; i++) {
      var place = widget.places?[i];

      final google_maps_flutter.LatLng googleMapsLatLng =
          google_maps_flutter.LatLng(
              place?.latitude ?? 0.0, place?.longitude ?? 0.0);
      google_maps_flutter.BitmapDescriptor icon =
          _customIcons[place?.imageUrl] ??
              google_maps_flutter.BitmapDescriptor.defaultMarker;

      final google_maps_flutter.Marker marker = google_maps_flutter.Marker(
        markerId:
            google_maps_flutter.MarkerId('${place?.title ?? "Marker"}_$i'),
        // Use index to ensure uniqueness
        position: googleMapsLatLng,
        icon: icon,
        infoWindow: google_maps_flutter.InfoWindow(
            title: place?.title, snippet: place?.description),
        onTap: () async {
          final callback = widget.onClickMarker;
          if (callback != null) {
            await callback(place);
          }
        },
      );
...
}

Again, click Save, then click the compile button (button with a hammer).

Geopoint Code

There is one small change to make it work with Geopoints. change the line that sets the coordinate variable to the one below.

 final latlng.LatLng coordinates = latlng.LatLng(
          place?.geopoint?.latitude ?? 0.0, place?.geopoint?.longitude ?? 0.0);

Using our New Custom Map Widget

Let’s use our new custom Widget. On the page I want to use my map on go to the widget tree menu and click the top level widget.

Also click on State Management to create a Local Page State Variable. Remember in our custom Widget we created a parameter to return a placeRow for the customer Marker that was clicked. We will be storing it here.

Click Add Field, set the Field name as place, of Type Document, and Collection Type place

Create another page state variable such as firebaseItems.

Now click Add Query

Choose Query Collection, choose the place Collection, and set Query Type as List of Documents. Click Confirm

Chain an action to the Firestore Query to update the page state using the firebaseList action output.

Define the Update Page State action

Find your custom Widget in the Widget Pallete.

I have a new project, so by default I have a column in my page. I dropped my custom widget here.

Now click on your custom Widget and update the parameters.

For the places parameter, you can click the orange icon setting and you will see firebaseItems Documents from Page State. This is from our Firestore Collection query we created earlier.

I also have defaultZoom and an action to set our page parameter when a custom marker is clicked. Click the Open button to create your action

Notice at the top of the popup it mentions it’s a callback. This simply means we are able to get an value back from any action. Click Add Action

Under State Management choose Update Page State

Click Add Field

Click our page state place from the popup.

Set update type to Set Value

For Value to set, under Callback Parameters choose placeRow. This is our Action parameter in our custom Widget.

Close and save the action.

For our custom Widget, under Padding & Alignment, I also chose Expanded .

Including Google Map API Key and local images for our Custom Widget

There are some minor limitations when using Google Maps and images in Flutter Flow. They won’t include the Google Map API key and local images if it does not see the Flutter Flow Google Map and Flutter Flow ImageView on any page.

In our case we might want to load a local asset image by specifying the name in the database.

To get around this create a blank page and drop Flutter Flows Google Map and ImageViews with the images you want to be included. You don’t have to have any navigation to these pages.

Test the custom Google Map with custom Markers

In my previous articles, at the time the custom Google Map wouldn’t render in Test Mode. With Flutter Flow’s latest updates it now loads our custom Google Map!

There is still one downside, the custom markers still don’t load in Test Mode. In all other modes it should work, I always download the project anyway.

What’s Next?

If you want to get better at custom Flutter development I recommend this book

Congratulations on implementing your new custom Google Map!

Flutter Flow: Scan a vehicle’s VIN to get the information

Flutter Flow: Scan a vehicle’s VIN to get the vehicle information

Previously I demonstrated how to enter the VIN in a Text Field to get vehicle information.
Flutter Flow: Get vehicle information from the VIN using the NHTSA API

I will enhance this by scanning a VIN barcode and automatically fetching the vehicle information.

Add a new button for scanning

I added a button titled Scan Vin.

For the action, you can open the Action Flow Editor on the Get Vehicle Info button, click on the three dots of the first action, and click Copy Action Chain.

You can then open the Action Flow Editor of the Scan Vin button and click Paste Actions.

Add one action above the action variable that uses the Flutter Flow Scan Barcode/QR Code (under Widget/UI interactions)

I set the action output as barcode

In Action 2, update the vin value in the API call to use the Action Output barcode

I also updated the Action Output for the API call to vinResults

Update this in the Conditional Action.

The next 2 actions are the same as before.

If all goes well you should get the following result, Congratulations!

I have created a form to better understand everyone’s needs. You can also request custom Flutter Flow development and/or Flutter Flow training.

https://coffeebytez.com/contact-me/

Flutter Flow: Use Supabase to show custom markers on Google Maps

Flutter Flow: Use Supabase to show custom markers on Google Maps

We previously discussed showing custom markers in Google Maps. We were adding data to the app state manually in that approach.
Flutter Flow: Adding Custom Markers for Google Maps

This might not be the most scalable solution. I’m going to discuss a better approach on how to read the data from Supabase to show custom markers in Google Maps.

If you are looking for the Firebase version, follow this article:
Flutter Flow: Google Map Custom Markers with Firebase

Update : The Supabase Community posted this article on their Twitter/X account. To celebrate I have updated this article with improvements to the code. Most notably adding the ability to load network images as icons.

Custom development

I get a lot of requests for custom tweaks of Google Maps with Flutter.

I have created a form to better understand everyone’s needs. You can also request custom Flutter Flow development and/or Flutter Flow training.

https://coffeebytez.com/contact-me/

I will focus on the most highly requested items first, which will be available on my substack. I will also post exclusive in-depth tutorials there.

Please subscribe to my substack here:

https://coffeebytez.substack.com/?r=gdmkm&utm_campaign=pub-share-checklist

If you have urgent specific requests, please leave your contact information in the survey.Setting up Supabase

You can finish the setup of Supabase and Flutter Flow by following the official Flutter Flow docs
Supabase Setup

In Supabase, go to the Table Editor menu tab. Let’s create a new table called place.

Give it the same columns as our custom data type from the last example, GoogleMapDataStruct. Notice instead of iconPath, we are using icon_name.

Using a type of varchar may be appropriate depending on your use case. Refer to the Reddit post below
https://www.reddit.com/r/PostgreSQL/comments/c38hgy/data_type_when_do_you_choose_varchar_over_text/


If you don’t have another means of adding data yet, you can add it manually by clicking Insert > Insert Row

Also, make sure to set a read policy on the place table. You can do this by going to the Authentication menu tab on the left.

For this example, I’m allowing read access to everyone.

Click New Policy > Get started quickly

You can use the first template given

Querying Supabase from our Custom Widget

Note: Flutter Flow now has the option to pass a list of Supabase Rows as a parameter and allows you have callbacks as parameters. I have submitted the new approach to the Flutter Flow Marketplace. The new approach is in the article below.
Flutter Flow: Google Maps Custom Marker Actions

We could query and then set it to app state and add a listener to FFAppState since it extends ChangeNotifier. The problem with this is that it will listen to every state change, even unrelated state changes.

Another approach is to directly query the table in Supabase. I chose to go with this approach for this example.

Let’s create another custom widget, this time naming it GoogleMapsSupabase.

The main changes I’ve made here are instead of creating the BitmapDescriptor in initState, I am first loading the data from Supabase from the place table.

Notice we are still using the custom data type from the previous example. We are mapping the results from Supabase to the custom data type GoogleMapDataStruct. We have also changed _createMarkers to use the **_fetchedMapData **variable.

Next, create the same parameters we used in the last example except for the mapData parameter. Notice we have also removed that from the example.

Note: Make sure to keep your images small

// Automatic FlutterFlow imports
import '/backend/schema/structs/index.dart';
import '/backend/supabase/supabase.dart';
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/custom_code/actions/index.dart'; // Imports custom actions
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'index.dart'; // Imports other custom widgets

import 'package:google_maps_flutter/google_maps_flutter.dart'
    as google_maps_flutter;
import '/flutter_flow/lat_lng.dart' as latlng;
import 'dart:async';
export 'dart:async' show Completer;
export 'package:google_maps_flutter/google_maps_flutter.dart' hide LatLng;
export '/flutter_flow/lat_lng.dart' show LatLng;
import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart';
import 'dart:ui';

// Set your widget name, define your parameter, and then add the
// boilerplate code using the green button on the right!

class GoogleMapsSupabase extends StatefulWidget {
  const GoogleMapsSupabase({
    Key? key,
    this.width,
    this.height,
    this.allowZoom = true,
    this.showZoomControls = true,
    this.showLocation = true,
    this.showCompass = false,
    this.showMapToolbar = false,
    this.showTraffic = false,
    this.centerLat = 0.0,
    this.centerLng = 0.0,
  }) : super(key: key);

  final double? width;
  final double? height;
  final bool allowZoom;
  final bool showZoomControls;
  final bool showLocation;
  final bool showCompass;
  final bool showMapToolbar;
  final bool showTraffic;
  final double centerLat;
  final double centerLng;

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

class _GoogleMapsSupabaseState extends State<GoogleMapsSupabase> {
  Completer<google_maps_flutter.GoogleMapController> _controller = Completer();
  Map<String, google_maps_flutter.BitmapDescriptor> _customIcons = {};
  Set<google_maps_flutter.Marker> _markers = {};
  List<GoogleMapDataStruct> _fetchedMapData = [];

  late google_maps_flutter.LatLng _center;

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

    _center = google_maps_flutter.LatLng(widget.centerLat, widget.centerLng);

    _loadData();
  }

  void _loadData() async {
    final supabase = SupaFlow.client;
    final response = await supabase.from('place').select().limit(10).execute();
    final data = response.data;

    if (data != null) {
      setState(() {
        _fetchedMapData = List<GoogleMapDataStruct>.from(
          data.map(
            (item) => GoogleMapDataStruct(
              latLng: latlng.LatLng(item['lat'], item['lng']),
              iconPath: item['icon_name'],
              title: item['title'],
              description: item['description'],
            ),
          ),
        );
      });

      _loadMarkerIcons();
    }
  }

  Future<void> _loadMarkerIcons() async {
    Set<String> uniqueIconPaths =
        _fetchedMapData.map((data) => data.iconPath).toSet() ??
            {}; // Extract unique icon paths

    for (String path in uniqueIconPaths) {
      if (path.isNotEmpty) {
        if (path.contains("https")) {
          Uint8List? imageData = await loadNetworkImage(path);
          if (imageData != null) {
            google_maps_flutter.BitmapDescriptor descriptor =
                google_maps_flutter.BitmapDescriptor.fromBytes(imageData);
            _customIcons[path] = descriptor;
          }
        } else {
          google_maps_flutter.BitmapDescriptor descriptor =
              await google_maps_flutter.BitmapDescriptor.fromAssetImage(
            const ImageConfiguration(devicePixelRatio: 2.5),
            "assets/images/$path",
          );
          _customIcons[path] = descriptor;
        }
      }
    }

    _updateMarkers(); // Update markers once icons are loaded
  }

  Future<Uint8List?> loadNetworkImage(String path) async {
    final completer = Completer<ImageInfo>();
    var image = NetworkImage(path);
    image.resolve(const ImageConfiguration()).addListener(ImageStreamListener(
        (ImageInfo info, bool _) => completer.complete(info)));
    final imageInfo = await completer.future;
    final byteData =
        await imageInfo.image.toByteData(format: ImageByteFormat.png);
    return byteData?.buffer.asUint8List();
  }

  void _updateMarkers() {
    setState(() {
      _markers = _createMarkers();
    });
  }

  .....

  Set<google_maps_flutter.Marker> _createMarkers() {
    var tmp = <google_maps_flutter.Marker>{};

    _fetchedMapData.forEach((mapData) {
      // Directly use the latlng.LatLng object
      final latlng.LatLng coordinates = mapData.latLng as latlng.LatLng;

      // Convert to google_maps_flutter.LatLng
      final google_maps_flutter.LatLng googleMapsLatLng =
          google_maps_flutter.LatLng(
              coordinates.latitude, coordinates.longitude);

      google_maps_flutter.BitmapDescriptor icon =
          _customIcons[mapData.iconPath] ??
              google_maps_flutter.BitmapDescriptor.defaultMarker;

      // Create and add the marker
      final google_maps_flutter.Marker marker = google_maps_flutter.Marker(
        markerId: google_maps_flutter.MarkerId(mapData.title),
        position: googleMapsLatLng,
        icon: icon,
        infoWindow: google_maps_flutter.InfoWindow(
            title: mapData.title, snippet: mapData.description),
      );

      tmp.add(marker);
    });

    return tmp;
  }
}

Get the full code here:
Flutter Flow: Use Supabase to show custom markers on Google Maps

Check out my other Flutter Flow and Supabase articles
Migrating away from Firebase, to Netlify and Supabase
FlutterFlow Stepper Form with Firebase Firestore
Flutter Flow: Get Value From Multiple-Choice Choice Chips
Flutter Flow: Carousel Menu
Flutter Flow: Migrating the Carousel Menu to use data types
Flutter Flow: Adding Custom Markers for Google Maps
Flutter Flow: Use Supabase to show custom markers on Google Maps

Flutter Flow: Google Maps Custom Marker Actions

Flutter Flow: Google Maps Custom Marker Actions

Previously I discussed how to implement Custom Markers with and without Supabase on the Flutter Flow platform.
Flutter Flow: Use Supabase to show custom markers on Google Maps
Flutter Flow: Adding Custom Markers for Google Maps

Flutter Flow has made a lot of updates and improvements since then, including allowing you to pass Supabase Rows and Actions (callbacks).

I’m going to improve the previous code to make it more flexible and update page state with the Place Row whenever a custom marker is clicked. This results in a lot less code.

I am also submitting this code as a free template on the Flutter Flow Market Place.

Custom development

I get a lot of requests for custom tweaks of Google Maps with Flutter.

I have created a form to better understand everyone’s needs. You can also request custom Flutter Flow development and/or Flutter Flow training.

https://coffeebytez.com/contact-me/

I will focus on the most highly requested items first, which will be available on my substack. I will also post exclusive in-depth tutorials there.

Please subscribe to my substack here:

https://coffeebytez.substack.com/?r=gdmkm&utm_campaign=pub-share-checklist

If you have urgent specific requests, please leave your contact information in the survey.

New Parameters

We have a few new parameters that will allow us to make the code more flexible and reusable across different pages and projects.

  • places — We will load the places table at a top level component of the page we include on the map.
  • **defaultZoom — **previously I set the default zoom at 11.0, I now have this as a parameter
  • onClickMarker — This is an action callback that returns the placeRow of the custom marker that was pressed

Note: The only nullable parameters are width, height, and onClickMarker. I have renamed lat > latitude and lng > longitude. I have also renamed Custom Widget to SupabaseMap. I may be better to click the duplicate widget button on your Custom Widget.

Also refer to the following article on adding the third party Google Map dependencies.
Flutter Flow: Adding Custom Markers for Google Maps

Improving The Code

Now let’s improve the code.

I’ll note a few improvements I made to this code.

  • We no longer use GoogleMapDataStruct.
  • Center is now a late initialized variable
  • Completely removed the loadData function.
  • Completely removed _fetchedMapData variable and replaced it with our places parameter.
  • Clicking on a map marker returns that markers placeRow object.
  • Added the ability to load network images (I’ve added this to the previous articles as well). For local asset images, you just give it the name like before.
........

class SupabaseMap extends StatefulWidget {
  const SupabaseMap({
    super.key,
    this.width,
    this.height,
    this.places,
    required this.centerLatitude,
    required this.centerLongitude,
    required this.showLocation,
    required this.showCompass,
    required this.showMapToolbar,
    required this.showTraffic,
    required this.allowZoom,
    required this.showZoomControls,
    required this.defaultZoom,
    this.onClickMarker,
  });

  final double? width;
  final double? height;
  final List<PlaceRow>? places;
  final double centerLatitude;
  final double centerLongitude;
  final bool showLocation;
  final bool showCompass;
  final bool showMapToolbar;
  final bool showTraffic;
  final bool allowZoom;
  final bool showZoomControls;
  final double defaultZoom;
  final Future Function(PlaceRow? placeRow)? onClickMarker;

  @override
  State<SupabaseMap> createState() => _SupabaseMapState();
}

class _SupabaseMapState extends State<SupabaseMap> {
  Completer<google_maps_flutter.GoogleMapController> _controller = Completer();
  Map<String, google_maps_flutter.BitmapDescriptor> _customIcons = {};
  Set<google_maps_flutter.Marker> _markers = {};

  late google_maps_flutter.LatLng _center;

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

    _center = google_maps_flutter.LatLng(
        widget.centerLatitude, widget.centerLongitude);

    _loadMarkerIcons();
  }

  @override
  void didUpdateWidget(SupabaseMap oldWidget) {   
      super.didUpdateWidget(oldWidget);  
      if (oldWidget.places != widget.places) { 
          _loadMarkerIcons();   
      }  
  }

  Future<void> _loadMarkerIcons() async {
    Set<String?> uniqueIconPaths =
        widget.places?.map((data) => data.imageUrl).toSet() ??
            {}; // Extract unique icon paths

    for (String? path in uniqueIconPaths) {
      if (path != null && path.isNotEmpty) {
        if (path.contains("https")) {
          Uint8List? imageData = await loadNetworkImage(path);
          if (imageData != null) {
            google_maps_flutter.BitmapDescriptor descriptor =
                await google_maps_flutter.BitmapDescriptor.fromBytes(imageData);
            _customIcons[path] = descriptor;
          }
        } else {
          google_maps_flutter.BitmapDescriptor descriptor =
              await google_maps_flutter.BitmapDescriptor.fromAssetImage(
            const ImageConfiguration(devicePixelRatio: 2.5),
            "assets/images/$path",
          );
          _customIcons[path] = descriptor;
        }
      }
    }

    _updateMarkers(); // Update markers once icons are loaded
  }


............


  Set<google_maps_flutter.Marker> _createMarkers() {
    var tmp = <google_maps_flutter.Marker>{};
    for (int i = 0; i < (widget.places ?? []).length; i++) {
      var place = widget.places?[i];
     
      final google_maps_flutter.LatLng googleMapsLatLng =
          google_maps_flutter.LatLng(
              place?.latitude ?? 0.0, place?.longitude ?? 0.0);

      google_maps_flutter.BitmapDescriptor icon =
          _customIcons[place?.imageUrl] ??
              google_maps_flutter.BitmapDescriptor.defaultMarker;

      final google_maps_flutter.Marker marker = google_maps_flutter.Marker(
        markerId:
            google_maps_flutter.MarkerId('${place?.title ?? "Marker"}_$i'),
        // Use index to ensure uniqueness
        position: googleMapsLatLng,
        icon: icon,
        infoWindow: google_maps_flutter.InfoWindow(
            title: place?.title, snippet: place?.description),
        onTap: () async {
          final callback = widget.onClickMarker;
          if (callback != null) {
            await callback(place);
          }
        },
      );

      tmp.add(marker);
    }
    return tmp;
  }

  @override
  Widget build(BuildContext context) {
    return google_maps_flutter.GoogleMap(
      onMapCreated: _onMapCreated,
      zoomGesturesEnabled: widget.allowZoom,
      zoomControlsEnabled: widget.showZoomControls,
      myLocationEnabled: widget.showLocation,
      compassEnabled: widget.showCompass,
      mapToolbarEnabled: widget.showMapToolbar,
      trafficEnabled: widget.showTraffic,
      initialCameraPosition: google_maps_flutter.CameraPosition(
        target: _center,
        zoom: widget.defaultZoom,
      ),
      markers: _markers,
    );
  }
}

Get the full code here:

Flutter Flow Custom Marker Actions

Updating he Supabase place table

I’ve also made some small adjustments to the place table. Refer to the following article for the initial setup of Supabase.
Flutter Flow: Use Supabase to show custom markers on Google Maps

The changes are just renaming a few columns. You can keep them the same and just update the column names in the code above.

  • lat > latitude
  • lng > longitude
  • icon_path > image_url

Using our new Custom Widget

Let’s use our new custom Widget. On the page I want to use my map on go to the widget tree menu and click the top level widget.

Also click on State Management to create a Local Page State Variable. Remember in our custom Widget we created a parameter to return a placeRow for the customer Marker that was clicked. We will be storing it here.

Click on Add Field. Give the field a name such as selectedPlace of type Supabase Row. Also choose your table name, in my case place.

Click on Add Field. Give the field a name such as selectedPlace of type Supabase Row. Also choose your table name, in my case place.

Now click Add Query

Choose Supabase Query as the Query Type, choose your place table, add whatever filters you want, and click Confirm. Also dont forget to create an Action Output variable name such as supabaseList. I’m keeping it simple and choosing not to use filters for this demo.

Add another action to the backend call to update the supabaseItems Page State with the supabaseList Action Output variable

Find your custom Widget in the Widget Pallete.

I have a new project, so by default I have a column in my page. I dropped my custom widget here.

Now click on your custom Widget and update the parameters. Notice we are using the page state variable supabaseItems for the places parameter.

For the places parameter, you can click the orange icon setting and you will see place Rows. This is from our Supabase query we created earlier.

I also have defaultZoom and an action to set our page parameter when a custom marker is clicked. Click the Open button to create your action

Notice at the top of the popup it mentions it’s a callback. This simply means we are able to get an value back from any action. Click Add Action

Under State Management choose Update Page State

Click Add Field

Click our page state selectedPlace from the popup.

Set update type to Set Value

Click UNSET. Under callback parameters choose placeRow. This is our action parameter returned in from our custom Widget.

Close and save the action. For our custom Widget, under Padding & Alignment, I also chose Expanded .

Including Google Map API Key and local images for our Custom Widget

There are some minor limitations when using Google Maps and images in Flutter Flow. They won’t include the Google Map API key and local images if it does not see the Flutter Flow Google Map and Flutter Flow ImageView on any page.

In our case we might want to load a local asset image by specifying the name in the database.

To get around this create a blank page and drop Flutter Flows Google Map and ImageViews with the images you want to be included. You don’t have to have any navigation to these pages.

Test the custom Google Map with custom Markers

In my previous articles, at the time the custom Google Map wouldn’t render in Test Mode. With Flutter Flow’s latest updates it now loads our custom Google Map!

There is still one downside, the custom markers still don’t load in Test Mode. In all other modes it should work, I always download the project anyway.

What’s Next?

I am submitting this to the FlutterFlow marketplace for free, to make it easy for anyone to use without going through this process. I will add the link below.

If you want to get better at custom Flutter development I recommend this book (affiliate link)

Note: While the Custom Widget is in the approval process, you will not be able to add it to your projects.

https://marketplace.flutterflow.io/item/zeSFD2KlIXUOfcXYwi1e

Also we are not doing anything with the page state from the selected Marker. I plan to create another article to make use of this. Stay tuned!

I also will have a video of this same process so you can follow along that way if you choose.

Congratulations on implementing your new custom Google Map!

If you want to get better at custom Flutter development I recommend this book