mapbox_maps_flutter 3.0.0-alpha.31 copy "mapbox_maps_flutter: ^3.0.0-alpha.31" to clipboard
mapbox_maps_flutter: ^3.0.0-alpha.31 copied to clipboard

Official Mapbox Maps SDK for Flutter. Add an interactive map widget that renders Mapbox Vector Tile (MVT) maps. Customize it for Android, iOS, and Web.

example/lib/main.dart

import 'package:flutter/material.dart';
// The SDK exports its own Size type, which shadows the Flutter geometry Size
// used by CustomPainter and PreferredSize below.
import 'package:mapbox_maps_flutter/mapbox_maps_flutter.dart' hide Size;
import 'package:url_launcher/url_launcher.dart';

import 'embed_url_strategy.dart';
import 'example.dart';
import 'examples.dart';
import 'theme.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  installEmbedAwareUrlStrategy();
  MapboxOptions.setAccessToken(const String.fromEnvironment('ACCESS_TOKEN'));
  runApp(const ExampleApp());
}

/// Example app shell.
///
/// Routes are hash URLs, `/#/<slug>`, because a static bucket cannot rewrite
/// requests.
class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Mapbox Maps Flutter examples',
      debugShowCheckedModeBanner: false,
      theme: buildExampleTheme(),
      initialRoute: '/',
      onGenerateRoute: _generateRoute,
    );
  }

  Route<void> _generateRoute(RouteSettings settings) {
    // `settings.name` is the hash route, e.g. `/full_map?hideAppBar=true`, so
    // the slug and any embedding query parameters must be split before use.
    final uri = Uri.parse(settings.name ?? '/');
    final slug = uri.path.replaceAll('/', '');
    if (slug.isEmpty) {
      return MaterialPageRoute(
        settings: settings,
        builder: (_) => const CatalogPage(),
      );
    }

    final example = exampleForSlug(slug);
    final embed = EmbedOptions.fromQuery(uri.queryParameters);
    return MaterialPageRoute(
      settings: settings,
      builder: (_) => example == null
          ? _UnknownExamplePage(slug: slug)
          : _ExamplePage(example: example, embed: embed),
    );
  }
}

/// Catalog of every example available on this platform, grouped by category.
class CatalogPage extends StatefulWidget {
  const CatalogPage({super.key});

  @override
  State<CatalogPage> createState() => _CatalogPageState();
}

class _CatalogPageState extends State<CatalogPage> {
  final _searchController = TextEditingController();
  var _query = '';

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  /// Examples matching the search box, grouped by category, in declaration
  /// order.
  Map<ExampleCategory, List<Example>> get _sections {
    final query = _query.trim().toLowerCase();
    final sections = <ExampleCategory, List<Example>>{};
    for (final example in examples) {
      if (!_matches(example, query)) continue;
      sections.putIfAbsent(example.category, () => []).add(example);
    }
    return Map.fromEntries(
      ExampleCategory.values
          .where(sections.containsKey)
          .map((category) => MapEntry(category, sections[category]!)),
    );
  }

  /// Matches a lowercased [query] against the title, subtitle and slug. An
  /// empty query matches everything.
  static bool _matches(Example example, String query) {
    if (query.isEmpty) return true;
    return example.title.toLowerCase().contains(query) ||
        (example.subtitle?.toLowerCase().contains(query) ?? false) ||
        example.slug.contains(query);
  }

  @override
  Widget build(BuildContext context) {
    final sections = _sections;
    final total = sections.values.fold(0, (sum, list) => sum + list.length);

    final bottomInset = MediaQuery.viewPaddingOf(context).bottom;
    return Scaffold(
      body: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 760),
          child: Column(
            children: [
              SafeArea(
                bottom: false,
                child: _Header(
                  controller: _searchController,
                  onChanged: (value) => setState(() => _query = value),
                  resultCount: total,
                  isSearching: _query.trim().isNotEmpty,
                ),
              ),
              Expanded(
                child: total == 0
                    ? const _EmptyResults()
                    : ListView(
                        // Scrolls under the home indicator etc., rather than
                        // stopping at it, so the list reaches the true bottom
                        // of the screen. The extra bottom padding keeps the
                        // last item clear of that system chrome once scrolled
                        // all the way down.
                        padding: EdgeInsets.fromLTRB(
                          16,
                          8,
                          16,
                          16 + bottomInset,
                        ),
                        children: [
                          for (final entry in sections.entries) ...[
                            Padding(
                              padding: const EdgeInsets.fromLTRB(4, 14, 4, 6),
                              child: GlassSectionLabel(entry.key.label),
                            ),
                            _ExampleGroup(examples: entry.value),
                          ],
                        ],
                      ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class _Header extends StatelessWidget {
  const _Header({
    required this.controller,
    required this.onChanged,
    required this.resultCount,
    required this.isSearching,
  });

  final TextEditingController controller;
  final ValueChanged<String> onChanged;
  final int resultCount;
  final bool isSearching;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              const _MapboxMark(size: 24),
              const SizedBox(width: 10),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(
                      'Maps SDK for Flutter',
                      style: theme.textTheme.titleLarge?.copyWith(
                        fontSize: 17,
                        color: Colors.white,
                      ),
                    ),
                    Text(
                      isSearching
                          ? '$resultCount matching examples'
                          : '$resultCount examples',
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: MapboxGlass.labelFaint,
                      ),
                    ),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 10),
          TextField(
            controller: controller,
            onChanged: onChanged,
            style: const TextStyle(color: Colors.white, fontSize: 13),
            decoration: InputDecoration(
              hintText: 'Search examples',
              isDense: true,
              prefixIcon: const Icon(
                Icons.search,
                size: 17,
                color: MapboxGlass.labelFaint,
              ),
              suffixIcon: isSearching
                  ? IconButton(
                      icon: const Icon(Icons.close, size: 18),
                      color: MapboxGlass.labelFaint,
                      tooltip: 'Clear search',
                      onPressed: () {
                        controller.clear();
                        onChanged('');
                      },
                    )
                  : null,
            ),
          ),
        ],
      ),
    );
  }
}

/// One category's examples, drawn as a single bordered group.
class _ExampleGroup extends StatelessWidget {
  const _ExampleGroup({required this.examples});

  final List<Example> examples;

  @override
  Widget build(BuildContext context) {
    return DecoratedBox(
      decoration: BoxDecoration(
        color: MapboxColors.gray90,
        borderRadius: BorderRadius.circular(MapboxRadius.large),
        border: Border.all(color: MapboxGlass.border),
      ),
      child: Column(
        children: [
          for (var i = 0; i < examples.length; i++) ...[
            if (i > 0) const Divider(height: 1, color: MapboxGlass.border),
            _ExampleTile(
              example: examples[i],
              isFirst: i == 0,
              isLast: i == examples.length - 1,
            ),
          ],
        ],
      ),
    );
  }
}

class _ExampleTile extends StatelessWidget {
  const _ExampleTile({
    required this.example,
    required this.isFirst,
    required this.isLast,
  });

  final Example example;
  final bool isFirst;
  final bool isLast;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    const corner = Radius.circular(MapboxRadius.large);
    final subtitle = example.subtitle;

    return InkWell(
      onTap: () => Navigator.of(context).pushNamed('/${example.slug}'),
      borderRadius: BorderRadius.only(
        topLeft: isFirst ? corner : Radius.zero,
        topRight: isFirst ? corner : Radius.zero,
        bottomLeft: isLast ? corner : Radius.zero,
        bottomRight: isLast ? corner : Radius.zero,
      ),
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
        child: Row(
          children: [
            IconTheme(
              data: const IconThemeData(
                color: MapboxGlass.labelMuted,
                size: 18,
              ),
              child: example.leading,
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    children: [
                      Flexible(
                        child: Text(
                          example.title,
                          style: theme.textTheme.titleSmall?.copyWith(
                            color: Colors.white,
                            fontSize: 14,
                          ),
                        ),
                      ),
                      if (example.docsUrl != null) ...[
                        const SizedBox(width: 8),
                        const _TutorialBadge(),
                      ],
                    ],
                  ),
                  if (subtitle != null && subtitle.isNotEmpty) ...[
                    const SizedBox(height: 2),
                    Text(
                      subtitle,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                      style: theme.textTheme.bodySmall?.copyWith(
                        color: MapboxGlass.labelFaint,
                        height: 1.35,
                      ),
                    ),
                  ],
                ],
              ),
            ),
            const SizedBox(width: 12),
            const Icon(
              Icons.chevron_right,
              size: 18,
              color: MapboxGlass.labelFaint,
            ),
          ],
        ),
      ),
    );
  }
}

class _EmptyResults extends StatelessWidget {
  const _EmptyResults();

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.search_off, size: 32, color: MapboxGlass.labelFaint),
          const SizedBox(height: 12),
          Text(
            'No examples match that search.',
            style: Theme.of(
              context,
            ).textTheme.bodyMedium?.copyWith(color: MapboxGlass.labelMuted),
          ),
        ],
      ),
    );
  }
}

/// The Mapbox mark.
class _MapboxMark extends StatelessWidget {
  const _MapboxMark({this.size = 24});

  final double size;

  @override
  Widget build(BuildContext context) {
    return SizedBox.square(
      dimension: size,
      child: CustomPaint(painter: _MapboxMarkPainter()),
    );
  }
}

class _MapboxMarkPainter extends CustomPainter {
  // Path data from the official Mapbox brand icon, on a 177.8 unit canvas.
  static const _viewBox = 177.8;

  @override
  void paint(Canvas canvas, Size size) {
    final scale = size.width / _viewBox;
    canvas.scale(scale);

    final fill = Paint()..color = MapboxColors.blue50;
    canvas.drawCircle(const Offset(88.9, 88.9), 88.9, fill);

    // The rounded "swoosh" and the four-point star, punched out in white.
    final mark = Paint()..color = Colors.white;
    final swoosh = Path()
      ..moveTo(131.1, 110.7)
      ..cubicTo(100.7, 141.1, 46.4, 131.4, 46.4, 131.4)
      ..cubicTo(36.6, 77.2, 67.1, 46.7, 67.1, 46.7)
      ..cubicTo(84.0, 29.8, 112.0, 30.5, 129.7, 48.1)
      ..cubicTo(147.4, 65.7, 148.0, 93.8, 131.1, 110.7)
      ..close();
    canvas.drawPath(swoosh, mark);

    final star = Path()
      ..moveTo(99.1, 52.1)
      ..lineTo(90.4, 70.0)
      ..lineTo(72.5, 78.7)
      ..lineTo(90.4, 87.4)
      ..lineTo(99.1, 105.3)
      ..lineTo(107.8, 87.4)
      ..lineTo(125.7, 78.7)
      ..lineTo(107.8, 70.0)
      ..close();
    canvas.drawPath(star, Paint()..color = MapboxColors.blue50);
  }

  @override
  bool shouldRepaint(_MapboxMarkPainter oldDelegate) => false;
}

class _ExamplePage extends StatelessWidget {
  const _ExamplePage({
    required this.example,
    this.embed = const EmbedOptions(),
  });

  final Example example;
  final EmbedOptions embed;

  @override
  Widget build(BuildContext context) {
    final showDocsLink = example.docsUrl != null && !embed.hideDocsLink;
    return Scaffold(
      appBar: embed.hideAppBar
          ? null
          : AppBar(
              title: Text(example.title),
              bottom: const PreferredSize(
                preferredSize: Size.fromHeight(1),
                child: Divider(height: 1),
              ),
            ),
      body: example.builder(context),
      bottomNavigationBar: showDocsLink
          ? _DocsUrlBar(url: example.docsUrl!)
          : null,
    );
  }
}

/// Marks a catalog entry that a documentation page publishes.
class _TutorialBadge extends StatelessWidget {
  const _TutorialBadge();

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
      decoration: BoxDecoration(
        color: MapboxColors.blue90,
        borderRadius: BorderRadius.circular(MapboxRadius.tiny),
        border: Border.all(color: MapboxColors.blue60),
      ),
      child: const Text(
        'TUTORIAL',
        style: TextStyle(
          fontSize: 9,
          fontWeight: FontWeight.w700,
          letterSpacing: 0.6,
          color: MapboxColors.blue10,
        ),
      ),
    );
  }
}

/// Opens the documentation page that publishes an example.
class _DocsUrlBar extends StatelessWidget {
  const _DocsUrlBar({required this.url});

  final String url;

  Future<void> _open(BuildContext context) async {
    final opened = await launchUrl(
      Uri.parse(url),
      mode: LaunchMode.externalApplication,
    );
    if (opened || !context.mounted) return;
    // Nothing handled the URL — show it so it can still be copied.
    ScaffoldMessenger.of(
      context,
    ).showSnackBar(SnackBar(content: Text('Could not open $url')));
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      top: false,
      child: DecoratedBox(
        decoration: const BoxDecoration(
          color: MapboxColors.gray95,
          border: Border(top: BorderSide(color: MapboxGlass.border)),
        ),
        child: InkWell(
          onTap: () => _open(context),
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
            child: Row(
              children: [
                const Icon(
                  Icons.menu_book_outlined,
                  size: 15,
                  color: MapboxColors.blue40,
                ),
                const SizedBox(width: 8),
                const Expanded(
                  child: Text(
                    'Open this example on docs.mapbox.com',
                    style: TextStyle(
                      fontSize: 12,
                      fontWeight: FontWeight.w600,
                      color: MapboxColors.blue40,
                    ),
                  ),
                ),
                const Icon(
                  Icons.open_in_new,
                  size: 14,
                  color: MapboxColors.blue40,
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _UnknownExamplePage extends StatelessWidget {
  const _UnknownExamplePage({required this.slug});

  final String slug;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Example not found')),
      body: Center(
        child: ConstrainedBox(
          constraints: const BoxConstraints(maxWidth: 420),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Icon(
                Icons.map_outlined,
                size: 40,
                color: MapboxGlass.labelFaint,
              ),
              const SizedBox(height: 16),
              Text(
                'No example is registered for "$slug" on this platform.',
                textAlign: TextAlign.center,
                style: Theme.of(
                  context,
                ).textTheme.bodyMedium?.copyWith(color: MapboxGlass.labelMuted),
              ),
              const SizedBox(height: 20),
              FilledButton(
                onPressed: () => Navigator.of(
                  context,
                ).pushNamedAndRemoveUntil('/', (route) => false),
                child: const Text('Back to all examples'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
381
likes
160
points
108k
downloads
screenshot

Documentation

API reference

Publisher

verified publishermapbox.com

Weekly Downloads

Official Mapbox Maps SDK for Flutter. Add an interactive map widget that renders Mapbox Vector Tile (MVT) maps. Customize it for Android, iOS, and Web.

Homepage
Repository (GitHub)
View/report issues
Contributing

Topics

#mapbox #map #maps #vector-tiles #mvt

License

BSD-3-Clause, MIT (license)

Dependencies

flutter, mapbox_maps_flutter_mobile, mapbox_maps_flutter_platform_interface, mapbox_maps_flutter_web, meta, turf

More

Packages that depend on mapbox_maps_flutter

Packages that implement mapbox_maps_flutter