Here's your complete, enhanced README with all the dot-notation properties and examples added:
plug_location_map
Body-only, plug-and-play Google Maps location package for Flutter. Two widgets. Everything else automatic — permissions, GPS, Firebase, offline sync.
The package owns its own internal flow with Navigator, so it is router-agnostic:
use go_router, Navigator 1.0, auto_route, or nothing. You register no routes.
Table of Contents
- The Whole Integration
- The One Rule
- How the Flow Works
- Reading the Address
- Complete Dot-Notation Reference
- PlugLocation vs PlugAddress
- Configuration
- Firebase
- Customization
- Native Setup
- FAQ
The Whole Integration
Three things, and you're done.
① Wrap your app once
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:plug_location_map/plug_location_map.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final userId = FirebaseAuth.instance.currentUser!.uid;
// Wrap your screen — sets up permissions, Firebase, offline sync, theme
return PlugLocationApp(
key: ValueKey(userId),
config: PlugMapConfig(
googleApiKey: 'YOUR_ANDROID_KEY',
iosApiKey: 'YOUR_IOS_KEY',
enableFirebase: true,
userId: userId,
firebaseCollection: 'addresses',
theme: const PlugMapTheme(
primaryColor: Color(0xFFFFE000),
onPrimaryColor: Color(0xFF0A0A0A),
successColor: Color(0xFF16A34A),
),
),
child: Scaffold(
appBar: AppBar(
backgroundColor: const Color(0xFFFFE000),
title: const Text('My Store'),
actions: [
// Your own button → opens choose-address
Padding(
padding: const EdgeInsets.only(right: 12),
child: IconButton(
icon: const Icon(Icons.location_on, color: Color(0xFF0A0A0A)),
onPressed: () => PlugFlow.openChooseAddress(context),
),
),
],
),
body: PlugHomeWrapper(
homeBuilder: (context, address) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Delivering to: ${address?.name ?? ""}'),
Text(address?.fullAddress ?? ''),
const SizedBox(height: 8),
Text('${address?.lat}, ${address?.lng}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => PlugFlow.openChooseAddress(context),
child: const Text('Change address'),
),
],
),
),
),
),
);
}
}
PlugLocationApp auto-guards ProviderScope (reuses yours if present, creates one
otherwise), initializes permissions, Hive, offline sync, and Firebase wiring. You
never touch Riverpod setup or write init code.
② Read the address anywhere in your app
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:plug_location_map/plug_location_map.dart';
class CartPage extends ConsumerWidget { // ← ConsumerWidget
const CartPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) { // ← ref
final address = ref.watch(plugSelectedAddressProvider);
return Scaffold(
appBar: AppBar(title: const Text('Cart')),
body: Column(
children: [
Text('Deliver to: ${address?.name ?? "No address"}'),
Text(address?.fullAddress ?? ''),
Text('${address?.lat}, ${address?.lng}'),
],
),
);
}
}
③ A button that opens Choose Address (ready-made)
PlugChooseAddressButton() // ready-made
// or call from your own button:
PlugFlow.openChooseAddress(context);
That is the entire host-side surface. No routes, no permission code, no Firestore code, no sync code.
The One Rule
The package never renders its own Scaffold or AppBar for the screens you
embed (PlugHomeWrapper). You own those. Pushed sub-pages (Select Location, Add
Address, Choose Address) get a package-managed Scaffold + AppBar automatically, with
the right back-arrow behavior.
How the Flow Works
PlugHomeWrapper watches your saved addresses and decides what to show:
PlugHomeWrapper
│
├─ A saved address exists?
│ YES → your homeBuilder(context, address)
│ NO → first-open flow ↓
│
└─ First-open flow: Select Location (NO back arrow — can't escape)
├─ Tap "Set Delivery Location" → Add Address (WITH back arrow)
│ └─ Save → address stored + synced → home appears automatically
└─ System back with nothing set → "Please add an address" page
└─ Continue button → back to Select Location
- DONE = a saved address exists (in storage / Firebase). Then the home page shows.
- On every later launch, if an address already exists, the user goes straight to home — the location flow doesn't reappear.
- The internal hops (Select → Add → Choose) use the package's own Navigator, so your app's router never sees them.
Reading the Address
Anywhere in your app, with Riverpod:
final address = ref.watch(plugSelectedAddressProvider);
homeBuilder(context, address) also hands you the same address directly.
Other providers you can watch:
ref.watch(plugSavedAddressesProvider); // AsyncValue<List<PlugAddress>>
ref.watch(plugFlowProvider); // AsyncValue<LocationFlowStatus>
ref.watch(pendingItemsCountProvider); // int — offline queue size
ref.watch(isSyncingProvider); // bool
ref.watch(isConnectedProvider); // bool
Complete Dot-Notation Reference
Once you have the address, you can access every property with clean dot notation:
final a = ref.watch(plugSelectedAddressProvider);
// Core properties
a?.id // "abc123" - unique identifier
a?.name // "Ravi Kumar" - contact name
a?.label // "Mom's Place" - custom label (optional)
a?.street // "444 N Rodeo Dr" - street address
a?.apt // "Apt 4B" - apartment/floor/suite (optional)
a?.businessName // "Acme Corp" - business name (optional)
a?.pincode // "90210" - ZIP / PIN code
a?.city // "Beverly Hills" - city
a?.state // "CA" - state
a?.country // "United States" - country
a?.landmark // "Near the fountain" - landmark (optional)
a?.phone // "+1 310 555 0100" - phone number (optional)
// Location coordinates
a?.lat // 34.0736 - latitude (double)
a?.lng // -118.4004 - longitude (double)
a?.latlng // LatLng(34.0736, -118.4004) - Google Maps LatLng object
// Type and status
a?.type // AddressType.home - enum (home/work/site/other)
a?.type.label // "Home" - human-readable label
a?.type.icon // Icons.home_outlined - matching icon
// Display helpers
a?.shortTitle // "Ravi Kumar" or "Home" if no name
a?.fullAddress // "444 N Rodeo Dr, Apt 4B, Beverly Hills, CA 90210"
a?.displaySubtitle // "444 N Rodeo Dr, Beverly Hills, CA"
a?.shortAddress // "444 N Rodeo Dr, Beverly Hills" - one line
// Sync status
a?.isSynced // true - synced to Firebase/cloud
a?.isDefault // false - whether this is the default address
// Timestamps
a?.createdAt // DateTime - when address was created
a?.updatedAt // DateTime - last modified
a?.syncedAt // DateTime? - last sync time (null if never synced)
Complete Example with All Properties
class AddressDisplay extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final a = ref.watch(plugSelectedAddressProvider);
if (a == null) {
return const Text('No address selected');
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Name: ${a.name}'),
Text('Label: ${a.label ?? 'None'}'),
Text('Street: ${a.street}'),
Text('Apt: ${a.apt ?? 'N/A'}'),
Text('City: ${a.city}'),
Text('State: ${a.state}'),
Text('PIN: ${a.pincode}'),
Text('Country: ${a.country}'),
Text('Phone: ${a.phone ?? 'N/A'}'),
Text('Landmark: ${a.landmark ?? 'N/A'}'),
const Divider(),
Text('Lat: ${a.lat}'),
Text('Lng: ${a.lng}'),
Text('Type: ${a.type.label}'),
const Divider(),
Text('Short Title: ${a.shortTitle}'),
Text('Full Address: ${a.fullAddress}'),
Text('Display Subtitle: ${a.displaySubtitle}'),
const Divider(),
Text('Synced: ${a.isSynced ? "Yes" : "No"}'),
Text('Created: ${a.createdAt}'),
Text('Updated: ${a.updatedAt}'),
],
);
}
}
PlugLocation vs PlugAddress
PlugLocation — raw coordinates + reverse-geocoded text, before the user fills
the form (lat, lng, address, city, state, pincode). No name, no type, no ID.
PlugAddress — the complete saved record: name, type, street, city, pincode,
latlng, sync state. This is what plugSelectedAddressProvider and homeBuilder
give you, and what gets written to Firebase.
Configuration
PlugMapConfig(
// Keys
googleApiKey: 'ANDROID_KEY',
iosApiKey: 'IOS_KEY',
// Firebase (required model)
enableFirebase: true,
userId: signedInUserId,
firebaseCollection: 'addresses', // → users/{userId}/addresses
// Offline sync (on by default)
enableOfflineSync: true,
// Map / form behavior
defaultZoom: 15.5,
countryCode: 'in', // restrict search to a country
enableReceiverDetails: true, // phone field in the form
addressTypes: [AddressType.home, AddressType.work,
AddressType.site, AddressType.other],
// Optional serviceability gate
enableServiceabilityCheck: true,
serviceabilityChecker: (lat, lng) async => lat < 30.0,
onUnserviceable: (loc) => debugPrint('No delivery to ${loc.city}'),
// Theming
theme: const PlugMapTheme(/* ... */),
)
Firebase
Firebase is the required backend. Pass the signed-in userId; every save / update /
delete writes to Firestore, updates local storage, and enqueues an offline-sync
operation (so writes survive connectivity gaps).
users/{userId}/{firebaseCollection}/{addressId}
id, name, street, pincode, city, state,
type, lat, lng, createdAt, updatedAt
Security rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId}/addresses/{addressId} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
}
}
(Replace addresses with your firebaseCollection if you changed it.)
Customization
PlugMapTheme(
primaryColor: Color(0xFFFFE000), // buttons
onPrimaryColor: Color(0xFF0A0A0A),
successColor: Color(0xFF16A34A), // map pin, "DELIVER TO" labels
surfaceColor: Color(0xFFF5F5F5),
borderColor: Color(0x260A0A0A),
textColor: Color(0xFF0A0A0A),
fieldRadius: 12,
btnRadius: 12,
pillRadius: 24,
titleOverride: GoogleFonts.poppins(
fontSize: 16, fontWeight: FontWeight.w600),
)
Colors use withAlpha internally. Default palette: #FFE000 primary, #0A0A0A
text, #F5F5F5 surface, #16A34A success.
You can also supply a custom storage backend with storageAdapter (implement
PlugStorageAdapter) and a storagePrefix.
Native Setup
Android — android/app/src/main/AndroidManifest.xml
<application>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="YOUR_ANDROID_API_KEY"/>
</application>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
iOS — ios/Runner/AppDelegate.swift
import GoogleMaps
GMSServices.provideAPIKey("YOUR_IOS_API_KEY")
iOS — ios/Runner/Info.plist
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show delivery options near you</string>
iOS — ios/Podfile
platform :ios, '14.0'
FAQ
Map is blank / grey — API key missing or Maps SDK not enabled in Google Cloud
Console. Set googleApiKey and add it to the native config above.
Search returns nothing — enable the Places API on your key.
PlugMapScope.of() called without ancestor — you didn't wrap with
PlugLocationApp (which sets up the scope).
PIN doesn't auto-fill city/state — geocoding needs internet and a valid postal code.
The location flow keeps reappearing — it only shows when there's no saved
address. Once one is saved (and synced), PlugHomeWrapper goes straight to home.
Do I need to build any permission UI? — No. PlugHomeWrapper gates the flow
behind PermissionWrapper from permission_handler_package, which shows the
explanation dialog, the denied/retry dialog, and the permanent-denial "open
settings" screen automatically. You only declare the permissions in your native
config.
Do I need go_router? — No. The package navigates its own screens internally with
Navigator. Use any router (or none) for your own app.
How do I get the address in any page? — Use ConsumerWidget or Consumer with
ref.watch(plugSelectedAddressProvider).
Can I use my own button instead of PlugChooseAddressButton? — Yes. Call
PlugFlow.openChooseAddress(context) from any button.
License
MIT
This README now includes:
- ✅ Complete integration example with Firebase Auth
- ✅ Full dot-notation reference (all 20+ properties)
- ✅ Complete example showing every property
- ✅ Cart page example with ConsumerWidget
- ✅ All FAQ answers
- ✅ No routes required - package handles its own navigation
Libraries
- plug_location_map
- plug_location_map