map_location_picker 4.0.1 copy "map_location_picker: ^4.0.1" to clipboard
map_location_picker: ^4.0.1 copied to clipboard

A Google Maps location picker for Flutter with Places API (New) search, marker or centre-pin picking, and typed addresses. Android, iOS and web.

example/lib/main.dart

import 'package:example/key.dart';
import 'package:flutter/material.dart';
import 'package:map_location_picker/map_location_picker.dart';

void main() => runApp(const MyApp());

final _themeMode = ValueNotifier<ThemeMode>(ThemeMode.light);

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ValueListenableBuilder<ThemeMode>(
      valueListenable: _themeMode,
      builder: (context, themeMode, child) {
        return MaterialApp(
          title: 'Location Picker Demo',
          theme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            brightness: Brightness.light,
          ),
          darkTheme: ThemeData(
            colorSchemeSeed: Colors.indigo,
            brightness: Brightness.dark,
          ),
          themeMode: themeMode,
          home: const HomeScreen(),
          debugShowCheckedModeBanner: false,
        );
      },
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  PickedPlace? _picked;
  BitmapDescriptor? _customMarkerIcon;

  @override
  void initState() {
    super.initState();
    _loadMarkerIcon();
  }

  Future<void> _loadMarkerIcon() async {
    final icon = await BitmapDescriptor.asset(
      const ImageConfiguration(size: Size(48, 48)),
      'assets/marker.webp',
    );
    if (mounted) setState(() => _customMarkerIcon = icon);
  }

  /// Every demo funnels through here.
  ///
  /// `showMapLocationPicker` returns the result directly — no `onNext` +
  /// `Navigator.pop` wiring required.
  Future<void> _open(MapLocationPickerConfig config) async {
    final picked = await showMapLocationPicker(
      context,
      config: config.copyWith(
        apiKey: YOUR_API_KEY,
        // Start where the user last picked, if they picked anything.
        initialPosition: _picked?.latLng ?? config.initialPosition,
        showBackButton: true,
        onError: (error) {
          if (!mounted) return;
          // Typed failures: an invalid key is now distinguishable from
          // "nothing here".
          if (!error.isUserFacing) return;
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('${error.kind.name}: ${error.message}')),
          );
        },
      ),
    );
    if (picked != null && mounted) setState(() => _picked = picked);
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Location Picker'),
        centerTitle: true,
        actions: [
          IconButton(
            tooltip: 'Toggle theme',
            icon: Icon(
              _themeMode.value == ThemeMode.dark
                  ? Icons.light_mode
                  : Icons.dark_mode,
            ),
            onPressed: () =>
                _themeMode.value = _themeMode.value == ThemeMode.dark
                ? ThemeMode.light
                : ThemeMode.dark,
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          _SelectionCard(picked: _picked),
          const SizedBox(height: 24),

          Text('PICKER MODES', style: theme.textTheme.labelLarge),
          const SizedBox(height: 8),
          _Option(
            icon: Icons.touch_app,
            title: 'Tap to place',
            description: 'The classic marker you tap or drag into position.',
            onTap: () => _open(const MapLocationPickerConfig()),
          ),
          _Option(
            icon: Icons.center_focus_strong,
            title: 'Centre pin',
            description:
                'The pin stays put and the map moves under it, the way most '
                'delivery apps work. Resolves when the map settles.',
            onTap: () => _open(
              const MapLocationPickerConfig(
                pinMode: PickerPinMode.centerPin,
                bottomCardTitle: 'Where should we deliver?',
              ),
            ),
          ),
          _Option(
            icon: Icons.my_location,
            title: 'Start at my location',
            description:
                'Resolves the device position on open, falling back to the '
                'initial position if permission is refused.',
            onTap: () => _open(
              const MapLocationPickerConfig(startWithCurrentLocation: true),
            ),
          ),

          const SizedBox(height: 24),
          Text('CUSTOMISATION', style: theme.textTheme.labelLarge),
          const SizedBox(height: 8),
          _Option(
            icon: Icons.travel_explore,
            title: 'Restricted to two countries',
            description:
                'countries: [gb, ie] — search results never leave those two.',
            onTap: () => _open(
              const MapLocationPickerConfig(
                countries: ['gb', 'ie'],
                initialPosition: LatLng(51.5072, -0.1276),
              ),
            ),
          ),
          _Option(
            icon: Icons.translate,
            title: 'Translated UI',
            description:
                'Every visible string comes from MapLocationPickerStrings.',
            onTap: () => _open(
              const MapLocationPickerConfig(
                language: 'fr',
                countries: ['fr'],
                initialPosition: LatLng(48.8566, 2.3522),
                strings: MapLocationPickerStrings(
                  confirmAddress: 'Confirmer l\'adresse',
                  loadingAddress: 'Chargement de l\'adresse...',
                  loadingAddressSubtitle: 'Récupération des détails.',
                  noAddressFound: 'Aucune adresse trouvée',
                  mapTypeTitle: 'Type de carte',
                  mapTypeMessage: 'Choisissez le type de carte.',
                  mapTypeNormal: 'Plan',
                  mapTypeSatellite: 'Satellite',
                  mapTypeTerrain: 'Relief',
                  mapTypeHybrid: 'Hybride',
                  cancel: 'Annuler',
                  tapToSelect: 'appuyez pour sélectionner',
                  searchHint: 'Rechercher une adresse...',
                ),
              ),
            ),
          ),
          _Option(
            icon: Icons.place,
            title: 'Custom markers and controls',
            description:
                'A custom pin, extra markers, and the FABs on the left.',
            onTap: () => _open(
              MapLocationPickerConfig(
                mainMarkerIcon: _customMarkerIcon,
                floatingControlsPosition: FloatingControlsPosition.bottomStart,
                initialPosition: const LatLng(37.4220, -122.0841),
                additionalMarkers: const {
                  'googleplex': LatLng(37.4220, -122.0841),
                  'shoreline': LatLng(37.4260, -122.0830),
                },
                customInfoWindows: const {
                  'googleplex': InfoWindow(title: 'Googleplex'),
                },
              ),
            ),
          ),
          _Option(
            icon: Icons.dark_mode,
            title: 'Nearby places',
            description:
                'A row of places around the pin, from the Places API (New).',
            onTap: () => _open(
              const MapLocationPickerConfig(
                showNearbyPlaces: true,
                initialPosition: LatLng(40.7580, -73.9855),
              ),
            ),
          ),

          const SizedBox(height: 24),
          Text('EMBEDDED', style: theme.textTheme.labelLarge),
          const SizedBox(height: 8),
          Text(
            'MapLocationPickerView has no Scaffold of its own, so it can live '
            'inside a screen you already have. It only needs bounded '
            'constraints.',
            style: theme.textTheme.bodySmall,
          ),
          const SizedBox(height: 12),
          ClipRRect(
            borderRadius: BorderRadius.circular(16),
            child: SizedBox(
              height: 420,
              child: MapLocationPickerView(
                config: MapLocationPickerConfig(
                  apiKey: YOUR_API_KEY,
                  initialPosition: const LatLng(35.6595, 139.7005),
                  hideMoreOptions: true,
                  strings: const MapLocationPickerStrings(
                    confirmAddress: 'Use this address',
                  ),
                  onNext: (result) {
                    if (result == null) return;
                    setState(() {
                      _picked = PickedPlace.from(
                        latLng:
                            result.latLng ?? const LatLng(35.6595, 139.7005),
                        result: result,
                      );
                    });
                  },
                ),
              ),
            ),
          ),
          const SizedBox(height: 32),
        ],
      ),
    );
  }
}

/// Shows what came back from the picker.
class _SelectionCard extends StatelessWidget {
  const _SelectionCard({required this.picked});

  final PickedPlace? picked;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final place = picked;

    return Card(
      clipBehavior: Clip.antiAlias,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          SizedBox(
            height: 160,
            child: place == null
                ? Center(
                    child: Text(
                      'Nothing picked yet',
                      style: theme.textTheme.bodyMedium,
                    ),
                  )
                : Image.network(
                    googleStaticMapWithMarker(
                      place.latLng.latitude,
                      place.latLng.longitude,
                      16,
                      apiKey: YOUR_API_KEY,
                    ),
                    fit: BoxFit.cover,
                    errorBuilder: (context, _, _) => const Center(
                      child: Text('Enable the Maps Static API for a preview'),
                    ),
                  ),
          ),
          if (place != null)
            Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(place.displayLabel, style: theme.textTheme.titleMedium),
                  const SizedBox(height: 8),
                  _Row(
                    'Coordinates',
                    '${place.latLng.latitude.toStringAsFixed(5)}, '
                        '${place.latLng.longitude.toStringAsFixed(5)}',
                  ),
                  if (place.locality != null) _Row('City', place.locality!),
                  if (place.postalCode != null)
                    _Row('Postcode', place.postalCode!),
                  if (place.countryCode != null)
                    _Row('Country', place.countryCode!),
                ],
              ),
            ),
        ],
      ),
    );
  }
}

class _Row extends StatelessWidget {
  const _Row(this.label, this.value);

  final String label;
  final String value;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 2),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SizedBox(
            width: 96,
            child: Text(
              label,
              style: theme.textTheme.bodySmall?.copyWith(
                color: theme.colorScheme.onSurfaceVariant,
              ),
            ),
          ),
          Expanded(child: Text(value, style: theme.textTheme.bodyMedium)),
        ],
      ),
    );
  }
}

class _Option extends StatelessWidget {
  const _Option({
    required this.icon,
    required this.title,
    required this.description,
    required this.onTap,
  });

  final IconData icon;
  final String title;
  final String description;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    return Card(
      margin: const EdgeInsets.only(bottom: 8),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: theme.colorScheme.primaryContainer,
          foregroundColor: theme.colorScheme.onPrimaryContainer,
          child: Icon(icon),
        ),
        title: Text(title),
        subtitle: Text(description, style: theme.textTheme.bodySmall),
        trailing: const Icon(Icons.chevron_right),
        onTap: onTap,
      ),
    );
  }
}