Custom Map Styling with Flutter and MapMetrics
This tutorial will show you how to create custom map styles and integrate them with your Flutter MapMetrics applications.
Using MapMetrics Portal for Custom Styles
The MapMetrics Portal provides an intuitive interface for creating custom map styles that work seamlessly with Flutter applications.
Step 1: Create a Custom Style
- Visit MapMetrics Portal: Go to portal.mapmetrics.org
- Navigate to Styles: Click on the "Styles" section
- Create New Style: Click "New Style" and choose a template
- Customize Your Style: Use the visual editor to modify:
- Colors and themes
- Fonts and typography
- Map features (roads, buildings, water, etc.)
- Icons and symbols
- Save and Get URL: Save your style and copy the style URL — it will look like
https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY
Step 2: Use Custom Style in Flutter
The style URL is the initStyle field of MapOptions, not a top-level styleUrl: prop on the widget:
import 'package:flutter/material.dart';
import 'package:mapmetrics/mapmetrics.dart';
class CustomStyledMapScreen extends StatefulWidget {
@override
_CustomStyledMapScreenState createState() => _CustomStyledMapScreenState();
}
class _CustomStyledMapScreenState extends State<CustomStyledMapScreen> {
MapController? mapController;
String currentStyleUrl = '';
// Different style URLs from MapMetrics Portal
final Map<String, String> styleOptions = {
'Dark Theme':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
'Light Theme':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_LIGHT_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
'Satellite':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_SATELLITE_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
'Custom Brand':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_CUSTOM_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
};
@override
void initState() {
super.initState();
currentStyleUrl = styleOptions.values.first;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Custom Styled Map'),
actions: [
PopupMenuButton<String>(
onSelected: _changeStyle,
itemBuilder: (context) => styleOptions.keys.map((String key) {
return PopupMenuItem<String>(
value: key,
child: Text(key),
);
}).toList(),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Icon(Icons.style),
),
),
],
),
body: MapMetricsView(
options: MapOptions(
initStyle: currentStyleUrl,
initCenter: Position(-74.0060, 40.7128), // New York (lng, lat)
initZoom: 12.0,
),
onMapCreated: (MapController controller) {
setState(() {
mapController = controller;
});
},
onStyleLoaded: (StyleController style) {
print('Custom style loaded successfully!');
},
),
);
}
// `initStyle` only sets the style used the first time the native map is
// created — rebuilding MapOptions with a new initStyle does *not* swap it
// at runtime. Use MapController.setStyleUri instead.
Future<void> _changeStyle(String styleName) async {
final url = styleOptions[styleName] ?? currentStyleUrl;
setState(() {
currentStyleUrl = url;
});
await mapController?.setStyleUri(url);
}
}Dynamic Style Switching
Smooth Style Transitions
setStyleUri switches the style in place without destroying the native map view — it's specifically designed to avoid the crashes that come from tearing down and recreating the map, and onStyleLoaded fires again once the new style has finished loading:
class DynamicStyleScreen extends StatefulWidget {
@override
_DynamicStyleScreenState createState() => _DynamicStyleScreenState();
}
class _DynamicStyleScreenState extends State<DynamicStyleScreen> {
MapController? mapController;
bool isDarkMode = false;
String get currentStyleUrl => isDarkMode
? 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_DARK_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY'
: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_LIGHT_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Dynamic Style Switching'),
actions: [
Switch(
value: isDarkMode,
onChanged: (value) {
setState(() {
isDarkMode = value;
});
_updateMapStyle();
},
),
],
),
body: MapMetricsView(
options: MapOptions(
initStyle: currentStyleUrl,
initCenter: Position(-74.0060, 40.7128),
initZoom: 12.0,
),
onMapCreated: (controller) => mapController = controller,
onStyleLoaded: (style) => print('Style loaded'),
),
);
}
Future<void> _updateMapStyle() async {
await mapController?.setStyleUri(currentStyleUrl);
}
}Custom Map Layers
Adding Custom Overlays
There is no circles: widget property or Circle class. Overlays are CircleLayer entries in the declarative layers: list, built from Point(coordinates: Position(lng, lat)). Note that CircleLayer.radius is a pixel radius, not a geographic radius in meters like the original Circle.radius — there is no geo-radius circle in the SDK, so treat the values below as an on-screen size rather than a real-world 2000m/1000m footprint:
class CustomLayersScreen extends StatefulWidget {
@override
_CustomLayersScreenState createState() => _CustomLayersScreenState();
}
class _CustomLayersScreenState extends State<CustomLayersScreen> {
MapController? mapController;
final Position highlightCenter = Position(-74.0060, 40.7128); // New York
final Position restrictedCenter = Position(-73.9851, 40.7589); // Times Square
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Custom Layers')),
body: MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: highlightCenter,
initZoom: 12.0,
),
onMapCreated: (controller) => mapController = controller,
layers: [
CircleLayer(
points: [Point(coordinates: highlightCenter)],
radius: 60,
color: Colors.blue.withValues(alpha: 0.1),
strokeColor: Colors.blue,
strokeWidth: 3,
),
CircleLayer(
points: [Point(coordinates: restrictedCenter)],
radius: 35,
color: Colors.red.withValues(alpha: 0.2),
strokeColor: Colors.red,
strokeWidth: 2,
),
],
),
);
}
}Branded Map Styles
Creating Brand-Consistent Maps
class BrandedMapScreen extends StatefulWidget {
@override
_BrandedMapScreenState createState() => _BrandedMapScreenState();
}
class _BrandedMapScreenState extends State<BrandedMapScreen> {
MapController? mapController;
// Brand colors
final Color primaryColor = Color(0xFF1E88E5);
final Color secondaryColor = Color(0xFF42A5F5);
final Color accentColor = Color(0xFFFF5722);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Branded Map'),
backgroundColor: primaryColor,
),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_BRANDED_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(-74.0060, 40.7128),
initZoom: 12.0,
),
onMapCreated: (controller) => mapController = controller,
),
// Custom branded overlay
Positioned(
top: 20,
right: 20,
child: Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: primaryColor.withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
child: Text(
'Your Brand',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}
}Style Configuration Options
Advanced Style Settings
The SDK has no setLayoutProperty/setPaintProperty to reach into the base style and toggle visibility of its built-in buildings/labels/roads layers (see Change Layer Color and Change Label Case for the same constraint). The reliable way to offer a "Show Buildings" / "Show Labels" / "Show Roads" toggle is to author separate style variants in the MapMetrics Portal and switch between them with setStyleUri, or to design your own style so those feature groups are easy to reason about at the source. This example keeps the toggles as a UI placeholder, same as the original tutorial did, but is explicit about why they don't wire up to a single running style:
class AdvancedStyleScreen extends StatefulWidget {
@override
_AdvancedStyleScreenState createState() => _AdvancedStyleScreenState();
}
class _AdvancedStyleScreenState extends State<AdvancedStyleScreen> {
MapController? mapController;
bool showBuildings = true;
bool showLabels = true;
bool showRoads = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Advanced Style Options')),
body: Column(
children: [
// Style controls
Container(
padding: EdgeInsets.all(16),
color: Colors.grey[100],
child: Column(
children: [
SwitchListTile(
title: Text('Show Buildings'),
value: showBuildings,
onChanged: (value) {
setState(() {
showBuildings = value;
});
_logIntendedStyle();
},
),
SwitchListTile(
title: Text('Show Labels'),
value: showLabels,
onChanged: (value) {
setState(() {
showLabels = value;
});
_logIntendedStyle();
},
),
SwitchListTile(
title: Text('Show Roads'),
value: showRoads,
onChanged: (value) {
setState(() {
showRoads = value;
});
_logIntendedStyle();
},
),
],
),
),
// Map
Expanded(
child: MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(-74.0060, 40.7128),
initZoom: 15.0,
),
onMapCreated: (controller) => mapController = controller,
),
),
],
),
);
}
void _logIntendedStyle() {
// There is no runtime hook to toggle base-style layer visibility.
// In production, map each combination to a pre-authored style variant
// and call mapController?.setStyleUri(variantUrl) instead.
print('Style updated: Buildings=$showBuildings, Labels=$showLabels, Roads=$showRoads');
}
}Performance Optimization
Style Loading Optimization
class OptimizedStyleScreen extends StatefulWidget {
@override
_OptimizedStyleScreenState createState() => _OptimizedStyleScreenState();
}
class _OptimizedStyleScreenState extends State<OptimizedStyleScreen> {
MapController? mapController;
bool isStyleLoaded = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Optimized Style Loading')),
body: Stack(
children: [
MapMetricsView(
options: MapOptions(
initStyle:
'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
initCenter: Position(-74.0060, 40.7128),
initZoom: 12.0,
),
onMapCreated: (controller) => mapController = controller,
onStyleLoaded: (style) {
setState(() {
isStyleLoaded = true;
});
},
),
// Loading indicator
if (!isStyleLoaded)
Container(
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Loading custom map style...'),
],
),
),
),
],
),
);
}
}Best Practices
Style Design Tips
- Consistency: Use consistent colors and fonts across your app and map
- Accessibility: Ensure sufficient contrast for text and important features
- Performance: Keep style complexity reasonable for smooth rendering
- Branding: Integrate your brand colors and elements naturally
- Testing: Test your styles on different devices and screen sizes
Style Management
class StyleManager {
static const Map<String, String> predefinedStyles = {
'default':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=DEFAULT_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
'dark':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=DARK_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
'satellite':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=SATELLITE_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
'minimal':
'https://gateway.mapmetrics-atlas.net/styles/?fileName=MINIMAL_STYLE_ID/YOUR_STYLE.json&token=YOUR_API_KEY',
};
static String getStyleUrl(String styleName) {
return predefinedStyles[styleName] ?? predefinedStyles['default']!;
}
static bool isValidStyleUrl(String url) {
return url.startsWith('https://gateway.mapmetrics-atlas.net/styles/') &&
url.contains('fileName=') &&
url.contains('token=');
}
}Next Steps
Now that you understand custom styling, try:
Pro Tip: Use the MapMetrics Portal's style editor to create multiple variations of your map style for different use cases (dark mode, minimal view, detailed view, etc.), and switch between them at runtime with MapController.setStyleUri rather than trying to mutate a single loaded style's layers in place.