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!