baaba_location 0.1.0 copy "baaba_location: ^0.1.0" to clipboard
baaba_location: ^0.1.0 copied to clipboard

Foreground location for Flutter that never throws: one call runs permission, services, fix, fallback and address lookup, with filters, a setup doctor and a test double.

baaba_location #

Foreground location for Flutter that never throws. One call runs the whole flow — permission, location services, precision, the fix, a fallback to the last known location, and the address — and returns one typed result.

final result = await BaabaLocation.instance.get(withAddress: true);

if (result.isSuccess) {
  print(result.address?.formatted ?? result.position);
} else if (result.failure!.resolution != LocationResolution.none) {
  await BaabaLocation.instance.resolve(result.failure!); // one button: "Turn on location"
}
  • The package does the work. get() asks for permission (rationale, prompt, Settings with steps, via baaba_permission_handler), turns location on, upgrades to precise when the profile needs it, reuses a recent fix, falls back to the last known location, and looks up the address. result.steps says what it did.
  • Two dialogs never stack. Several get() calls at once — two widgets on one screen — share one flow.
  • A last known location that is almost always there. The package keeps its own copy, and picks the newer of it and the OS's.
  • Filters drop bad accuracy, impossible jumps, mocked fixes, 0,0 and duplicates before your code sees them.
  • Geocoding with the hard parts handled: a queue, back-off when throttled, a cache, locales (ur-PK addresses), cancellation, and pluggable providers with fallbacks.
  • A setup doctor. diagnose() prints exactly what to add to your manifest or Info.plist.
  • Nothing throws. Every failure is a typed LocationFailure with a resolution, reported once to your observer.
  • A test double (FakeBaabaLocation) that runs the real flow without a device.

Built on geolocator and geocoding. Android and iOS; on web and desktop every call returns notApplicable.

Contents #


60-second setup #

1. Add the package.

dependencies:
  baaba_location: ^0.1.0

2. Android — android/app/src/main/AndroidManifest.xml, inside <manifest>:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

and in android/app/build.gradle(.kts): compileSdk = 37 (needed by permission_handler, through baaba_permission_handler).

3. iOS — ios/Runner/Info.plist, inside <dict>:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Your location is used to show where you are.</string>

4. Call it.

final result = await BaabaLocation.instance.get(withAddress: true);

No init needed. If something is missing, the result is a misconfigured failure whose details['fix'] is the exact snippet to add — and BaabaLocation.instance.diagnose() prints a checklist.

Platform setup per feature #

baaba_location declares no permissions of its own. Your app declares exactly what it uses:

Feature Android (AndroidManifest.xml) iOS (Info.plist)
Any location (get, streams, last known) ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION NSLocationWhenInUseUsageDescription
Approximate only (no precise) ACCESS_COARSE_LOCATION alone —
Precise upgrade on iOS 14+ (navigation, fitness, or PrecisionNeed) — NSLocationTemporaryUsageDescriptionDictionary with a key; set LocationConfig.iosPrecisePurposeKey to it
Geocoding (platform geocoder) nothing nothing
Background tracking (BackgroundLocation) FOREGROUND_SERVICE + FOREGROUND_SERVICE_LOCATION; POST_NOTIFICATIONS to show the notification on 13+ UIBackgroundModes → location
Your own HTTP geocoding provider INTERNET —

What merges into your app from the plugins: geolocator's GeolocatorLocationService element (a location foreground service, started only by BackgroundLocation) and the Play services meta-data. No permissions — tool/check_merged_manifest.sh builds the example and fails if ACCESS_BACKGROUND_LOCATION or a library-added foreground-service permission appears.

ACCESS_BACKGROUND_LOCATION and "Always" permission are never needed.

Choosing a profile #

Profiles set accuracy, interval, distance filter, filters and precision together. Accuracy is always your choice: override it per call (get(accuracy: LocationAccuracy.high)) or copy a profile (LocationProfile.delivery.copyWith(interval: Duration(seconds: 10))).

Profile For Accuracy Interval / distance Reuses a fix up to Filters Precise
checkIn (default) one-off "where am I", attendance balanced (≈ 100 m) — 30 s — asked once
nearby store finder, address fill low (≈ 1–10 km) 30 s / 100 m 5 min — never
navigation live map, turn-by-turn best 1 s / 5 m 5 s ≤ 50 m, ≤ 70 m/s required
delivery courier, patrol high 5 s / 15 m 15 s ≤ 50 m, ≤ 50 m/s asked once
fitness runs, walks high 2 s / 5 m 5 s ≤ 30 m, ≤ 15 m/s required
batterySaver long sessions on old phones balanced 60 s / 100 m 2 min — never
LocationAccuracy Android iOS Typical
passive lowest 3 km only fixes other apps asked for
low low power 1 km city block to city
balanced balanced 100 m Wi-Fi and cell, ≈ 100 m
high high accuracy best GPS, ≈ 5–20 m
best high accuracy best for navigation GPS + sensor fusion on iOS

For attendance with a tight radius, use LocationProfile.checkIn.copyWith(accuracy: LocationAccuracy.high).

Getting a location #

final r = await BaabaLocation.instance.get(
  profile: LocationProfile.checkIn,
  withAddress: true,
  locale: const Locale('ur', 'PK'),   // address language
  context: context,                    // optional: where dialogs appear
);

The flow, in order — each step only when needed:

  1. Configuration — a missing manifest / Info.plist entry fails with misconfigured before anything is asked (on iOS that would otherwise be a silent denial).
  2. Permission — the baaba_permission_handler flow. handlePermission: false skips it; LocationConfig.permissionStep replaces it with your own UI.
  3. Services — Android's in-app "turn on location" dialog (with Play services), otherwise a dialog that opens location settings and waits for the return. Switching location on from the notification shade also counts. handleServices: false skips it.
  4. Precision — if the profile wants precise and only approximate is allowed, it asks once; if the user keeps approximate, the result is still a success with isApproximate: true (or approximateOnly for PrecisionNeed.required).
  5. Fix — a fix younger than the profile's maxAge is reused without waking the GPS (source: cached), otherwise one is requested.
  6. Fallback — no fix in time? The last known location up to maxFallbackAge (30 min) is returned as a success, source: lastKnownOs or lastKnownStored, with its age.
  7. Address — with withAddress. A geocoding failure never fails the result: address is null and addressFailure says why.

LocationResult carries position, address, addressFailure, source, age, isApproximate, isMocked, and steps. Read position only after isSuccess (or use positionOrNull).

The widget for the common case:

LocationBuilder(
  withAddress: true,
  builder: (context, result) =>
      Text(result.address?.formatted ?? result.position.toString()),
  // loadingBuilder / failureBuilder are optional; the default failure view
  // shows the message and a button that runs resolve(failure).
)

Streams #

// High level: setup once, then results (with the address if asked).
BaabaLocation.instance.watch(profile: LocationProfile.delivery, withAddress: true);

// Low level: fixes only, no UI.
BaabaLocation.instance.positionStream(
  accuracy: LocationAccuracy.high,
  distanceFilter: 20,
  interval: const Duration(seconds: 5),
);
  • Every listener shares one platform subscription that uses the most demanding settings; each listener still gets its own distance filter, interval and filters. The last cancel stops it.
  • Streams never error and never end because of state. When location services go off, permission changes or the app goes to the background, they pause; statusStream says why; they resume by themselves.
  • watch(withAddress: true) looks up the address again only after moving 50 m or after 10 minutes (GeocodingConfig.watchRegeocodeMeters / watchRegeocodeTtl), never per fix.

Background tracking (opt-in) #

By default streams pause in the background. Pass BackgroundLocation to keep one running — a delivery trip, a patrol, a run:

BaabaLocation.instance.positionStream(
  accuracy: LocationAccuracy.high,
  distanceFilter: 15,
  background: const BackgroundLocation(
    notificationTitle: 'Trip in progress',
    notificationText: 'Your location is shared until you end the trip.',
    notificationIcon: 'drawable/ic_tracking',
  ),
);
  • Android runs a location foreground service with that notification. Start the stream while the app is visible. Declare FOREGROUND_SERVICE and FOREGROUND_SERVICE_LOCATION; Google Play then asks for a foreground-service declaration in the Console.
  • iOS keeps updating in the background with the blue indicator. Needs UIBackgroundModes → location; without it the stream stays foreground-only and positionStream reports misconfigured.
  • While-in-use permission is enough.

BackgroundLocation also takes notificationChannelName ('Location'), notificationIcon ('mipmap/ic_launcher', as type/name in res/), notificationColor, ongoing (true), wakeLock / wifiLock (false; Android) and showIosIndicator (true).

What it does not do: fixes live only while the process lives. If the OS or the user kills the app, tracking stops, nothing is buffered on disk, and it does not restart after a reboot. Apps that need that use a dedicated background-location package.

Last known location #

final r = await BaabaLocation.instance.lastKnownPosition(maxAge: const Duration(hours: 1));
// r.source: lastKnownOs or lastKnownStored; r.age; r.address (if one was looked up)

The OS's last known location is often missing (new device, after a reboot, after location was off) or old, so the package keeps its own copy of the latest accepted fix (and its address), and returns the newer of the two — the more accurate one when they are within 30 s.

  • Stored in the app's cache directory, which neither Android Auto Backup nor iCloud backs up: a location is never restored onto another device. The OS may clear it when storage runs low.
  • Written at most every 25 m or 10 s (storeWriteMinDistance / storeWriteMinInterval), and when the app pauses.
  • Only fixes that pass LocationConfig.filters are stored — with rejectMocked, a spoofed fix is never kept.
  • On logout and account switch, call clearStoredLocation(). It deletes the stored copy and the address cache. Turn storing off with persistLastKnown: false. Nothing stored is ever sent anywhere.

Geocoding #

final address = await BaabaLocation.instance.addressFrom(position);
final address2 = await BaabaLocation.instance.addressFrom(null, lat: 31.52, lng: 74.35);
final places = await BaabaLocation.instance.positionsFrom('Mall Road, Lahore');

Address has formatted (one display line, built from the parts when the provider has none), name, street, houseNumber, subLocality, locality, subAdministrativeArea, administrativeArea, postalCode, country, countryCode, raw. Missing parts are null, never '', and nothing is invented — a city-only or plus-code-only answer still gives a usable formatted.

Handled for you:

  • Queue and throttling — one request at a time per provider; on rateLimited it backs off (1 s, doubling, up to maxBackoff) instead of failing every call.
  • Cache — reverse results by a ~20 m grid cell + locale, forward results by the normalised query + locale, with a TTL and a size cap. A stream drifting a few metres never triggers a lookup.
  • Cancellation — pass consumer:; a newer call with the same consumer completes the older one at once with superseded (a search box typing quickly sends only what's needed).
  • Locale — per call, then GeocodingConfig.locale, then the device's. Part of the cache key.
  • Fallback providers — on notSupported, network or rateLimited the next provider is tried; never on noResult.

The default provider is the platform geocoder (Android Geocoder, iOS CLGeocoder: free, no key). Plug in your own backend, Google or Mapbox:

class MyBackendGeocoder extends GeocodingProvider {
  @override
  String get name => 'backend';

  @override
  Future<AddressResult> reverse(double lat, double lng, {Locale? locale}) async { ... }

  @override
  Future<PositionsResult> forward(String query, {Locale? locale}) async { ... }
}

BaabaLocation.init(LocationConfig(
  geocoding: GeocodingConfig(fallbackProviders: [MyBackendGeocoder()]),
));

The example app has a complete OpenStreetMap Nominatim provider.

Permissions #

Call Shows UI
get(), watch(), LocationBuilder, ensurePermission(), resolve() yes — the baaba_permission_handler flow, with your BaabaPermissions.init wording and theme
currentPosition(), lastKnownPosition(), positionStream(), checkPermission() never — a missing permission is permissionDenied at once

LocationAccess reports state and precise. Approximate-only access (Android 12+ "Approximate", iOS 14+ Precise off) is its own state: access.approximateOnly. requestPrecise() asks for the upgrade (Android re-request; iOS temporary full accuracy, with iosPrecisePurposeKey).

Setup doctor #

final report = await BaabaLocation.instance.diagnose();
print(report.toChecklist());

In debug builds it also prints the checklist. It reports: missing manifest / Info.plist entries with the snippet to add, the iOS purpose key and background mode, permission and precision, location services, a short test fix (no UI), whether the geocoder is present and answering, the stored last known location, device model and OS version, and battery-optimisation state on Android.

With baaba_app_config, drop it into the debug panel in one line:

DebugSection.info('Location', () => report.toMap())

Error handling #

No public method throws. Each returns a result, and every failure:

  • has a reason from one closed enum, LocationFailureReason;
  • has a message for developers (not end users);
  • has a resolution — requestPermission, requestPrecise, openAppSettings, openLocationSettings, updatePlayServices, fixConfiguration, retryLater or none — that resolve(failure) performs;
  • carries details (the missing key, the snippet, the provider) and error / stackTrace where relevant;
  • reaches LocationObserver.onError(operation, failure, stack) exactly once, with a stable operation name: get, get.address, watch, watch.address, currentPosition, lastKnownPosition, positionStream, addressFrom, positionsFrom, serviceEnabled, checkPermission, ensurePermission, requestPrecise, openLocationSettings, openAppSettings, resolve, diagnose, storage.corrupt, storage.write, callback.permissionStep.

Nothing hangs: every platform call has a timeout, and a whole get() flow is bounded by flowTimeout (3 min). State changes — services off, permission downgraded — arrive on statusStream, not as errors.

Troubleshooting #

Reason What it means What diagnose() shows Fix
misconfigured A manifest / Info.plist entry is missing ✗ Missing … with the snippet Add the snippet
permissionDenied Not allowed yet Permission not granted yet resolve(failure)
permissionPermanentlyDenied Refused for good permanently denied resolve(failure) opens Settings with steps
permissionRestricted Parental controls / MDM (iOS) restricted Nothing the app can do
approximateOnly The profile needs precise Only approximate location is allowed resolve(failure); on iOS set iosPrecisePurposeKey
serviceDisabled Location is off Location services are off resolve(failure)
timeout No fix in time, and no last known within maxFallbackAge No fix: timeout Indoors? Try balanced, or a longer timeout
noFix The platform gave up, nothing stored No fix: noFix As above
notSupported No location hardware, or no platform geocoder No platform geocoder Add a fallback GeocodingProvider
network / rateLimited / providerError Geocoding failed The geocoder did not answer Retry later; add a fallback provider
noResult The geocoder has nothing there (sea, empty area) — Show coordinates instead
superseded A newer geocoding call with the same consumer replaced this one — Nothing: expected while typing
invalidInput An empty query, or coordinates out of range — Check the arguments
platformError An unexpected plugin error; see error — Report it; a "not registered" message means stop and rerun the app
notApplicable Web or desktop unsupported platform —

playServicesMissing, playServicesOutdated, startNotAllowed, storageFull, limitReached and cancelledByUser are reserved and never returned today; a switch over the enum still has to cover them.

Gotchas #

  • position throws on a failure. Check isSuccess first, or use positionOrNull.
  • balanced is about 100 m. Attendance with a tight radius needs high.
  • Geolocator allows one position stream per app. If your own code also calls Geolocator.getPositionStream(), the two share it — use BaabaLocation.instance.positionStream() instead.
  • iOS doesn't open location settings directly. The services dialog opens the app's Settings page; the user goes back to Privacy → Location Services from there.
  • Ages use the device clock (geolocator has no monotonic time). A clock moved backwards marks a stored value's age as unreliable instead of guessing.
  • A permission dialog still on screen when flowTimeout passes stays there until the user answers; the flow has already returned timeout and ignores the late answer.
  • Background tracking stops if the app is killed. See Background tracking.

Testing #

import 'package:baaba_location/testing.dart';

final location = FakeBaabaLocation(permission: LocationAccess.denied)
  ..simulateUserResponse(FakeUserResponse.grantPrecise)
  ..setCurrentFix(Position(latitude: 31.52, longitude: 74.35, timestamp: DateTime.now()));

final r = await location.get();
expect(r.isSuccess, isTrue);
expect(r.steps, contains(LocationStep.requestedPermission));
expect(location.callsTo('get'), hasLength(1));

FakeBaabaLocation is the real implementation on a simulated device, so the flow, filters, fallback, cache and geocoding queue behave as in production. Script it with emit, playRoute, setServiceEnabled, setPermission, setLastKnown / setOsLastKnown / setStoredLastKnown, failNext(reason), timeoutNext, setMisconfigured, setBackground, stubAddress, stubPositions, geocodingFails, setOnline and simulateUserResponse. Every call is recorded in calls. Use it in widget tests with LocationBuilder(location: fake, …) or BaabaLocation.instance = fake.

Configuration reference #

LocationConfig #

Field Default
observer null LocationObserver: onPosition, onServiceStatus, onPermission, onFiltered, onResult, onError
logging debug builds [baaba_location] log lines
logPrint debugPrint Where log lines go
defaultProfile checkIn For get() / watch() without a profile
filters null island + duplicates Applied to every fix and to what is stored
persistLastKnown true Keep the stored last known location
storeWriteMinDistance / storeWriteMinInterval 25 m / 10 s Write throttling
maxFallbackAge 30 min Oldest last known location get() falls back to
flowTimeout 3 min Bound on a whole get() flow
platformTimeout 10 s Bound on platform calls without their own timeout
permissionStep null Replace the permission step with your own UI
permissions BaabaPermissions.instance The permission handler to use
permissionOptions null Per-call PermissionRequestOptions
iosPrecisePurposeKey null Key in NSLocationTemporaryUsageDescriptionDictionary
geocoding GeocodingConfig() See below

GeocodingConfig #

Field Default
provider platform geocoder First provider
fallbackProviders [] Tried on notSupported, network, rateLimited
locale device Address language
reverseCachePrecisionMeters 20 Cache grid cell size, in metres (above zero)
cacheTtl / cacheSize 24 h / 200 Per direction
offlineCacheRangeMeters 200 When every provider fails with network, a cached address this close is returned
watchRegeocodeMeters / watchRegeocodeTtl 50 m / 10 min When watch looks up again
timeout 10 s Per call, queueing and back-off included
maxBackoff 30 s Back-off ceiling

PositionFilters #

Field Default
maxAccuracyMeters off Drop fixes less accurate than this
maxSpeedMps off Drop jumps faster than this (allowing for both fixes' accuracy; resets after 3 rejections or a 2-minute gap)
rejectMocked false Drop mocked fixes; they are reported to onFiltered
rejectNullIsland true Drop exactly 0,0
dropDuplicates true Drop duplicates and out-of-order fixes
jumpResetCount / jumpResetAfter 3 / 2 min When the jump filter gives up and accepts the new place

PositionFilters.standard is the default; PositionFilters.none turns every filter off.

Helpers #

BaabaLocation.distanceBetween(a, b) (metres) and BaabaLocation.bearingBetween(a, b) (degrees from north) take two LatLngs and work on every platform.

License #

MIT

0
likes
150
points
128
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Foreground location for Flutter that never throws: one call runs permission, services, fix, fallback and address lookup, with filters, a setup doctor and a test double.

Topics

#location #geolocation #geocoding #gps

License

MIT (license)

Dependencies

baaba_permission_handler, device_info_plus, flutter, geocoding, geolocator, geolocator_android, path_provider

More

Packages that depend on baaba_location

Packages that implement baaba_location