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

  1. The Whole Integration
  2. The One Rule
  3. How the Flow Works
  4. Reading the Address
  5. Complete Dot-Notation Reference
  6. PlugLocation vs PlugAddress
  7. Configuration
  8. Firebase
  9. Customization
  10. Native Setup
  11. 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