Haters of Google Maps rejoice!
Hey there Flutterflow community! I've been working on an app for several months now where maps are an integral feature. I know I'm not the only one who's tried to get Mapbox to play nice with Flutterflow, but after several weeks of struggle, I think i've finally got this square peg sort of through the round hole.
Replacing flutter-mapbox-gl
Like many others before me, I tried to get flutter-mapbox-gl to work with limited success. (Credit to tobrun, felix-ht, and the rest of the amazing people who did 99% of the work on this package, but it seems like they're not interested in maintaining the package anymore with the last version on pub.dev being 20 months old at this point.) I tried this package several times on a few different Flutterflow projects, but I just didn't understand Flutter or the structure of the package enough to debug the myriad compounding issues keeping it from working. On the latest try, I got lucky in isolating enough of the problems to get it to work on my local machine, but several out of date dependencies and errors (especially in the Android build file) meant it was never going to work straight out of Flutterflow—no less in test mode or anything like that.
Introducing mapbox-gl-flutterflow
Disclaimer: I am not affiliated with Mapbox or Flutterflow in any way, I just named the project something that should help people find it on Google.
In order to get the package working natively out of Flutterflow, it was necessary to implement the changes on a package hosted on pub.dev. Since the old package seemed to have no hope of a revival, I made the necessary changes and published mapbox-gl-flutterflow
and it's dependencies mapbox-gl-flutterflow-web
and mapbox-gl-flutterflow-platform-interface
on pub.dev. I'm currently importing these packages directly in Flutterflow and getting reasonably successful outputs.
I AM NOT A SOFTWARE ENGINEER
I'm doing my best to make this package as functional as possible, but I am not a software engineer and I don't understand what 50% of the code in this library does. I fixed the dependency incompatibilities with Flutterflow, updated the Android build spec, and did a ⌘F -> Replace changing "mapbox_gl" to "mapbox_gl_flutterflow" (half /s half serious). I know there's some issues, but it's a fairly functional starting point for other Flutterflowers to build on. The documentation on the original project was not amazing, so I'll do my best to share what I learn about using this library in the readme and elsewhere on the repo. If anyone smarter than me wants to contribute, please do.
Running on Flutterflow.
I've pasted a sample of my map custom widget down below. You only have to add the top level library as a dependency (https://pub.dev/packages/mapbox_gl_flutterflow). You WILL need to follow the "Setting Up" instructions. Add the HTML tags to your Flutterflow Project under Settings > Web Publishing > Custom Headers to get it to work on Web Publish/Test Mode. Follow the other instructions on your local machine for iOS and Android. I have had the most issues getting it to work in Test Mode. Web Publish and local development have been fairly straightforward.
Final Notes
On the Mapbox side of things, there's two types of access token you need, and they're not clearly differentiated. One is public: I believe you can safely expose this (on the web, for instance) as long as you've got your allowed origins configured properly, and you need it for the map to load. The other is private: This is the one you'll add in your .netrc and gradle.properties files so the correct mapbox native libraries can be downloaded to build those apps. You still need to include your public token in the widget somewhere for the maps to load.
Bug: There's an issue with the gestureRecognizers option. It works in the example code, doesn't work in my Flutterflow project. No idea why.
Bug: I don't know if its a mapbox problem, a mapbox-gl-flutterflow problem, or a Flutterflow problem, but custom styles won't load for me.
Annoying: Mapbox uses its own LatLng type, and I'm also using my own LatLon type. The former is not my fault, and I'm using my own elsewhere because Flutterflow doesn't expose the lat and lng properties of the build in LatLng for some reason.
Bonus: I'm including an example of loading in a custom marker image below. This method only works with raster image types (like .jpg and .png, not .svg), and this is why the Flutter services package is imported. That directory holds true for images uploaded to Flutterflow assets, but you have to enable "Include unused assets" if the map is the only place you use it. There's a way to load remote images too, but I didn't need to figure that out for this project.
Anyways, I hope this helps people who need a real map that isn't Google Maps in their project. Happy building!
// Automatic FlutterFlow imports
import '/backend/schema/structs/index.dart';
import '/backend/schema/enums/enums.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 'dart:math';
import 'package:flutter/services.dart';
import 'package:mapbox_gl_flutterflow/mapbox_gl_flutterflow.dart'
as mapbox; // Use 'mapbox' as a prefix
class FullMap extends StatefulWidget {
const FullMap({
Key? key,
this.width,
this.height,
required this.isMobile,
this.markers,
required this.markerClickedCallback,
required this.origin,
}) : super(key: key);
final double? width;
final double? height;
final bool isMobile;
final List<LatLonObjectStruct>? markers;
final Future<dynamic> Function() markerClickedCallback;
final LatLonObjectStruct origin;
@override
_FullMapState createState() => _FullMapState();
}
class _FullMapState extends State<FullMap> {
mapbox.MapboxMapController? mapController;
_onMapCreated(mapbox.MapboxMapController controller) {
mapController = controller;
}
_onStyleLoadedCallback() {
// ScaffoldMessenger.of(context).showSnackBar(SnackBar(
// content: Text("Style loaded: ${mapbox.MapboxStyles.LIGHT}"),
// backgroundColor: Theme.of(context).primaryColor,
// duration: Duration(seconds: ),
// ));
_updateMarkers();
addImageFromAsset("basic-marker", "assets/images/basic-marker.png");
debugPrint("style load cb");
}
Future<void> addImageFromAsset(String name, String assetName) async {
final bytes = await rootBundle.load(assetName);
final Uint8List list = bytes.buffer.asUint8List();
return mapController!.addImage(name, list);
}
void _addMarker(double lat, double lng) {
mapController?.addSymbol(
mapbox.SymbolOptions(
geometry: mapbox.LatLng(lat, lng), // Replace with your coordinates
iconImage: "basic-marker", // Replace with your marker icon
iconSize: .2,
),
);
}
void _onMarkerClickedCallback() {
debugPrint("marker click");
widget.markerClickedCallback();
}
@override
void didUpdateWidget(covariant FullMap oldWidget) {
super.didUpdateWidget(oldWidget);
debugPrint("didupdate");
if (oldWidget.markers != widget.markers) {
_updateMarkers();
debugPrint("_update triggered");
}
}
Future<void> _updateMarkers() async {
// Clear existing markers
debugPrint("_update called");
await mapController?.clearSymbols();
// Add new markers
if (widget.markers != null) {
for (var marker in widget.markers!) {
_addMarker(marker.lat, marker.lon);
}
}
}
// "mapbox://styles/buyblvdryan/clrff17o6004701re2j5thsbf"
@override
Widget build(BuildContext context) {
return new Scaffold(
body: mapbox.MapboxMap(
styleString: mapbox.MapboxStyles.LIGHT,
accessToken: <PUBLIC_ACCESS_TOKEN>,
onMapCreated: _onMapCreated,
initialCameraPosition: mapbox.CameraPosition(
target: mapbox.LatLng(
widget.origin.lat == 0 ? 34.083834 : widget.origin.lat,
widget.origin.lon == 0 ? -118.361486 : widget.origin.lon),
zoom: 11),
myLocationEnabled: false,
compassEnabled: false,
onStyleLoadedCallback: _onStyleLoadedCallback,
onAttributionClick: _onMarkerClickedCallback));
}
}
// Set your widget name, define your parameter, and then ad2d the
// boilerplate code using the green button on the right!