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

Google Map location picker for flutter Based on google_maps_flutter.

map_location_picker #

Pub Version Pub Points GitHub Repo stars GitHub Repo issues GitHub Repo contributors

A Google Maps location picker for Flutter, on the Places API (New).

Pick a point, get a typed address back. Works on Android, iOS and web — search included, with no CORS proxy.

Default View Dark Mode Custom Markers Custom Map Type Liquid Card

Quick start #

dependencies:
  map_location_picker: ^4.0.0
import 'package:map_location_picker/map_location_picker.dart';

final picked = await showMapLocationPicker(
  context,
  config: const MapLocationPickerConfig(apiKey: 'YOUR_API_KEY'),
);

if (picked != null) {
  print(picked.latLng);            // always present
  print(picked.formattedAddress);  // '10 Downing St, London SW1A 2AA, UK'
  print(picked.city);              // 'London'
  print(picked.countryCode);       // 'GB'
}

That is the whole integration. Everything below is optional.

Requires Flutter 3.38.1 / Dart 3.10. Upgrading from 3.x? See the migration guide.


Setup #

Get an API key from the Google Cloud console and enable, for the platforms you ship:

API Needed for
Maps SDK for Android / iOS rendering the map
Maps JavaScript API rendering the map on web
Places API (New) search and place details
Geocoding API turning coordinates into addresses
Maps Static API only for googleStaticMapWithMarker previews

Billing must be enabled on the project.

Enable Places API (New), not the legacy "Places API". This package uses the new endpoints; the legacy ones are closed to projects created after 1 March 2025.

Android #

android/app/src/main/AndroidManifest.xml:

<manifest>
  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
  <application>
    <meta-data android:name="com.google.android.geo.API_KEY"
               android:value="YOUR_API_KEY"/>
  </application>
</manifest>

minSdkVersion must be 21 or higher.

iOS #

ios/Runner/AppDelegate.swift:

import GoogleMaps

@main
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("YOUR_API_KEY")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

ios/Runner/Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Shows your current location on the map so you can pick an address.</string>

Only add NSLocationAlwaysUsageDescription or UIBackgroundModes: location if your app genuinely needs background location for something else. A location picker does not, and requesting it invites an App Store rejection.

Web #

Add the Maps JavaScript API to web/index.html, after <base href>:

<base href="$FLUTTER_BASE_HREF">

<script async
  src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&loading=async"></script>

That tag is what lets google_maps_flutter_web render the map. Search does not need it: this package calls the Places REST API directly, and places.googleapis.com supports CORS. No proxy is required.

If you restrict your key, use an HTTP referrer restriction matching your origin. Do not add custom headers on web — they turn a simple request into a preflight that maps.googleapis.com rejects.

Platform support #

Platform Status
Android
iOS
Web ✅ (JS and WebAssembly)
macOS / Windows / Linux ❌ — google_maps_flutter has no desktop implementation

Desktop is an upstream limitation, not something this package can work around.


Restricting the API key #

Restricting a key to your bundle identifier is strongly recommended. When you do, the REST calls need identifying headers:

import 'dart:io' show Platform;

final headers = <String, String>{
  if (Platform.isIOS || Platform.isMacOS)
    'X-Ios-Bundle-Identifier': 'com.example.app',
  if (Platform.isAndroid) ...{
    'X-Android-Package': 'com.example.app',
    // Base16 (hex) SHA-1 of the signing certificate, colons stripped.
    // keytool prints AA:BB:CC:... — remove the colons. Case does not matter.
    'X-Android-Cert': '00112233445566778899AABBCCDDEEFF00112233',
  },
};

MapLocationPickerConfig(
  apiKey: 'YOUR_API_KEY',
  geocodingApiHeaders: headers,              // Geocoding API
  placesApi: PlacesAPINew(                   // Places API (New)
    apiKey: 'YOUR_API_KEY',
    headers: headers,
  ),
)

Both are needed: geocodingApiHeaders only reaches the Geocoding client.

See Google's API security best practices.


Usage #

Picking modes #

// Tap or drag a marker (default).
const MapLocationPickerConfig(apiKey: key)

// A fixed pin with the map moving underneath, the way most delivery and
// ride-hailing apps work. Resolves when the map settles.
const MapLocationPickerConfig(apiKey: key, pinMode: PickerPinMode.centerPin)

Starting at the user's location #

const MapLocationPickerConfig(
  apiKey: key,
  startWithCurrentLocation: true,
  locationTimeout: Duration(seconds: 8),
)

Falls back to initialPosition if permission is refused or no fix arrives in time, so the picker is never blank.

const MapLocationPickerConfig(
  apiKey: key,
  countries: ['gb', 'ie'],                  // ISO 3166-1 alpha-2, up to 15
  placeTypes: [PlaceType.streetAddress],    // up to 5
  language: 'en',
)

For anything more specific, build the filter yourself — it wins over the shorthands above:

SearchConfig(
  searchFilter: AutocompleteSearchFilter(
    locationBias: /* ... */,
    includeQueryPredictions: true,
  ),
)

Embedding it in a screen you already have #

SizedBox(
  height: 420,
  child: MapLocationPickerView(         // no Scaffold of its own
    config: MapLocationPickerConfig(
      apiKey: key,
      onNext: (result) => print(result?.formattedAddress),
    ),
  ),
)

MapLocationPickerView needs bounded constraints. Use MapLocationPicker (which adds the Scaffold) when pushing a full-screen route.

Driving it programmatically #

final controller = MapLocationPickerController(
  config: const MapLocationPickerConfig(apiKey: key),
);

MapLocationPicker(config: config, controller: controller);

await controller.moveTo(const LatLng(48.8584, 2.2945));
await controller.goToCurrentLocation();
controller.setMapType(MapType.hybrid);
print(controller.address);

// It is a ChangeNotifier.
ListenableBuilder(
  listenable: controller,
  builder: (context, _) => Text(controller.address),
);

Remember to dispose() a controller you created.

Handling failures #

Every failure is typed. Previously an invalid key, an exceeded quota and "no results here" were all the same silent empty state.

MapLocationPickerConfig(
  apiKey: key,
  onError: (e) {
    switch (e.kind) {
      case MapPickerErrorKind.requestDenied:
        // Wrong key, key restrictions, or the API isn't enabled.
      case MapPickerErrorKind.quotaExceeded:
      case MapPickerErrorKind.network:
      case MapPickerErrorKind.locationPermissionDeniedForever:
        // Send them to system settings.
      default:
        break;
    }
  },
)

Translating the UI #

Every visible string lives on one object:

MapLocationPickerConfig(
  apiKey: key,
  strings: MapLocationPickerStrings(
    confirmAddress: AppLocalizations.of(context)!.confirmAddress,
    searchHint: AppLocalizations.of(context)!.searchHint,
    noAddressFound: AppLocalizations.of(context)!.noAddressFound,
    // ... 14 more, all with English defaults
  ),
)

Reading the result #

final picked = await showMapLocationPicker(context, config: config);

picked!.latLng;             // always present, even if geocoding failed
picked.name;                // 'Heathrow Terminal 5' — kept from search results
picked.formattedAddress;
picked.street;
picked.locality;
picked.postalCode;
picked.countryCode;
picked.result;              // the raw GeocodingResult
picked.place;               // the raw Places result, if search was used

Or work from a GeocodingResult directly:

result.city;
result.postalCode;
result.countryCode;
result.component('administrative_area_level_2');
result.latLng;

Nearby places #

const MapLocationPickerConfig(
  apiKey: key,
  showNearbyPlaces: true,
  nearbyPlacesRadius: 300,
  nearbyPlaceTypes: [PlaceType.restaurant, PlaceType.cafe],
)

The search field on its own #

PlacesAutocomplete(
  config: const SearchConfig(apiKey: 'YOUR_API_KEY'),
  onGetDetails: (place) => print(place?.formattedAddress),
  onError: (e) => print(e),
)

Customising the chrome #

MapLocationPickerConfig(
  apiKey: key,
  cardType: CardType.liquidCard,
  floatingControlsPosition: FloatingControlsPosition.bottomStart,
  showBackButton: true,
  bottomCardTitle: 'Where should we deliver?',
  mainMarkerIcon: myBitmapDescriptor,
  centerPinBuilder: (context, state) => MyPin(state: state),
  bottomCardBuilder: (context, result, results, address, isLoading, onNext, searchBar) {
    return MyCard(address: address, onConfirm: onNext);
  },
)

Costs #

Places autocomplete is billed per session, not per keystroke — but only if the session token is reused across the search and then retired by the details call. This package handles that for you. (Before 4.0.0 it did not: every keystroke opened its own session.)

To cut the Place Details bill, ask for fewer fields:

SearchConfig(
  apiKey: key,
  placesAllFields: false,
  placeFields: ['id', 'location', 'formattedAddress', 'displayName'],
)

See Places pricing.


Troubleshooting #

The suggestion list is always empty. Almost always the API key. Add an onError callback — MapPickerErrorKind.requestDenied means the key is wrong, restricted to a different app, or Places API (New) is not enabled.

"Confirm" does nothing / stays greyed out. Geocoding failed. onError will say why. The button still returns the raw coordinate unless you set requireGeocodedAddress: true.

The picker renders squashed in a corner. You nested MapLocationPicker (which contains a Scaffold) inside a Column or scroll view. Use MapLocationPickerView and give it bounded constraints.

Nothing is clickable on web. The map is an HTML platform view that wins hit-testing. The package wraps its own overlays in PointerInterceptor; if you stack your own widgets over the map, do the same.

The map is blank on Android. The com.google.android.geo.API_KEY meta-data is missing or the Maps SDK for Android is not enabled.


Contributing #

Issues and pull requests are welcome — see CONTRIBUTING.md.

Support #

BuyMeACoffee PayPal GitHub Sponsors

Contributors #