plug_location_map 1.0.1
plug_location_map: ^1.0.1 copied to clipboard
Plug-and-play Google Maps location package for Flutter. Body-only — host app provides its own AppBar. Handles permissions, GPS, Zomato map picker, Instacart address form, Firebase persistence, offline [...]
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