Flutter Flow: Map Custom Markers using Custom Data Types and Action Blocks
Flutter

Flutter Flow: Map Custom Markers using Custom Data Types and Action Blocks

2024-03-15
12 min read

Flutter Flow: Map Custom Markers using Custom Data Types and Action Blocks

Flutter Flow continues to develop and improve its platform.

My first article with custom markers we used custom data types with data from App State.

With the the introduction of Document and Supabase parameters, I changed it to allow you to first query places, without customizing the code.

Now it’s even easier and we are going back to Custom data types to make it more flexible to use with any backend service.

There are also minor cleanup and improved functionality.

Creating our custom Data Types

We are creating a few custom data types. Feel free to change them based on your specific needs

  • latitude: Our latitude coordinate

  • longitude: Our longitude coordinate

  • title: This is what shows on the popup when a marker is clicked

  • description: This shows under the title on the popup when the marker is clicked.

  • image_url: This is the image path of the custom marker (local or remote)

Mapping functions

There is currently not a built-in way to map Supabase or Firestore data to a Custom Data Type. For now, we will create custom functions to handle this.

Go to the Custom Code tab and create a new function. We will start with Supabase first.

Give the function a name such as createSupabasePlaceList.

For the Function Settings, give it a return value of a List of the Place Data Type.

Also, give a List of Supabase place rows as an argument. You can name this parameter places.

The code is pretty straight forward. We are going through the list of Supabase Rows and creating a new Place Data Type list.

import 'dart:convert';
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart' as timeago;
import '/flutter_flow/lat_lng.dart';
import '/flutter_flow/place.dart';
import '/flutter_flow/uploaded_file.dart';
import '/flutter_flow/custom_functions.dart';
import '/backend/backend.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '/backend/schema/structs/index.dart';
import '/backend/supabase/supabase.dart';

List<PlaceStruct>? createSupabasePlaceList(List<PlaceRow>? places) {
  /// MODIFY CODE ONLY BELOW THIS LINE

  return places?.map((place) {
    return PlaceStruct(
      title: place.title,
      description: place.description,
      latitude: place.latitude,
      longitude: place.longitude,
      imageUrl: place.imageUrl,
    );
  }).toList();

  /// MODIFY CODE ONLY ABOVE THIS LINE
}

For Firestore, we can duplicate the function, but we need to change the argument type to Document.

The function is similar, with the argument as the only difference.

import 'dart:convert';
import 'dart:math' as math;

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:timeago/timeago.dart' as timeago;
import '/flutter_flow/lat_lng.dart';
import '/flutter_flow/place.dart';
import '/flutter_flow/uploaded_file.dart';
import '/flutter_flow/custom_functions.dart';
import '/backend/backend.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '/backend/schema/structs/index.dart';
import '/backend/supabase/supabase.dart';

List<PlaceStruct>? createFirestorePlaceList(List<PlaceRecord>? places) {
  /// MODIFY CODE ONLY BELOW THIS LINE

  return places?.map((place) {
    return PlaceStruct(
      title: place.title,
      description: place.description,
      latitude: place.latitude,
      longitude: place.longitude,
      imageUrl: place.imageUrl,
    );
  }).toList();

  /// MODIFY CODE ONLY ABOVE THIS LINE
}

Custom Widget

Let’s create our new custom widget.

I gave it the name CustomDataTypeMap.

Let’s define the parameters first. I’ll start with the more advanced ones first.

The places parameter is what we will use to pass our list of the custom Place Data Type.

We also have onClickMarker. We can use this callback to retrieve the place whenever a marker is clicked.

The rest of the parameters are straight forward.

The code is pretty much the same, besides minor cleanup and improvements.

// Automatic FlutterFlow imports
import '/backend/backend.dart';
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 '/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 CustomDataTypeMap extends StatefulWidget {
  const CustomDataTypeMap({
    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<PlaceStruct>? 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(PlaceStruct? placeRow)? onClickMarker;

  @override
  State<CustomDataTypeMap> createState() => _CustomDataTypeMapState();
}

class _CustomDataTypeMapState extends State<CustomDataTypeMap> {
  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;

  final HTTPS_PATH = "https";
  final IMAGES_PATH = "assets/images/";
  final UNDERSCORE = '_';
  final MARKER = "Marker";

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

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

    _loadMarkerIcons();
  }

  @override
  void didUpdateWidget(CustomDataTypeMap 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_PATH)) {
          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),
            "$IMAGES_PATH$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();
    });
  }

  void _onMapCreated(google_maps_flutter.GoogleMapController controller) {
    _controller.complete(controller);
  }

  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 latlng.LatLng coordinates =
          latlng.LatLng(place?.latitude ?? 0.0, place?.longitude ?? 0.0);

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

      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?.name ?? MARKER}$UNDERSCORE$i'),
        position: googleMapsLatLng,
        icon: icon,
        infoWindow: google_maps_flutter.InfoWindow(
            title: place?.name, 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,
    );
  }
}

Remember on the first implementation we were getting errors when we didn’t include the dependencies?

Another great improvement is that we don’t have to rely on adding these dependencies anymore.

In the future, we will need to add them back for some new map features I’m working on. This is because the current version FlutterFlow uses is behind the version I need. Stay tuned…

Create a new Local Page State Variable named dataTypeItems that is a list of Place Data Type

Create Action Blocks

You don’t have to create an Action Block, but this makes it convenient when you have a large project and/or change the page a lot. It helps prevent accidentally messing up the action.

You can name the query something like **getDataTypePlaces. **It’s pretty simple. A backend call to get the data, then we pass the action output to our custom function.

Let’s look at Supabase first.

We can define the name and return value.

We query our place table and give it an action output of dataTypePlaces

Click the plus button under the Backend Call action and choose Add Return

Toggle Add Return Value, Choose our createSupabasePlaceList function, and for the value choose the Action Output variable dataTypePlaces

You should get the following

For Firebase the steps are pretty much the same. First, create the query and give the Action Output Variable a name such as firestoreDataList

Then we will use the createFirestorePlaceList function to create a list from firestoreDataList Action Output variable, so that we can return the list of Place Data Type.

After we exit the Action Block, click the Actions tab, and choose Add Action

Find your getDataTypePlaces action under Page Action Blocks.

Give the Action Output Variable a name such as placeList

Click the Open button next to Action Flow Editor

Click the plus button to chain the Update Page State action to our action block. We will use this to set Page State dataTypeItems to our placeList Action Output Variable

Drag your custom widget onto your page and set the page state variable dataTypeItems to your places parameter.

Run your app and you should get the following. Congratulations!