Skip to content

Both copy text to your clipboard — Build with AI copies a setup prompt to paste into Claude Code, Cursor, Codex or Copilot; Copy page as Markdown copies this page to paste into a chat. How it works

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

  1. Visit MapMetrics Portal: Go to portal.mapmetrics.org
  2. Navigate to Styles: Click on the "Styles" section
  3. Create New Style: Click "New Style" and choose a template
  4. Customize Your Style: Use the visual editor to modify:
    • Colors and themes
    • Fonts and typography
    • Map features (roads, buildings, water, etc.)
    • Icons and symbols
  5. 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:

dart
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:

dart
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:

dart
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

dart
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:

dart
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

dart
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

  1. Consistency: Use consistent colors and fonts across your app and map
  2. Accessibility: Ensure sufficient contrast for text and important features
  3. Performance: Keep style complexity reasonable for smooth rendering
  4. Branding: Integrate your brand colors and elements naturally
  5. Testing: Test your styles on different devices and screen sizes

Style Management

dart
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.