Week 12: Location, Sensors & Permissions

GPS coordinates and motion data are two more sensors readable directly from the device, both behind a runtime permission prompt that's easy to get wrong. This week reads both properly, and — building on Week 11's permission_handler use — gets the permission request itself right.

Phase 6 of 8 Week 12 of 20 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Read a device's current location, and watch it update over time, with geolocator
  • Read accelerometer data with sensors_plus
  • Request location permission with a clear rationale, and distinguish "while using the app" from "always" permission
  • Build a small feature combining both sensors

1. Reading Location with geolocator

terminal
flutter pub add geolocator
getting a one-off location, with the permission dance handled explicitly
Future<Position?> getCurrentLocation() async {
  if (!await Geolocator.isLocationServiceEnabled()) return null;

  var permission = await Geolocator.checkPermission();
  if (permission == LocationPermission.denied) {
    permission = await Geolocator.requestPermission();
    if (permission == LocationPermission.denied) return null;
  }
  if (permission == LocationPermission.deniedForever) return null;

  return Geolocator.getCurrentPosition(
    locationSettings: const LocationSettings(accuracy: LocationAccuracy.medium),
  );
}
watching location as it changes
final subscription = Geolocator.getPositionStream(
  locationSettings: const LocationSettings(
    accuracy: LocationAccuracy.medium,
    distanceFilter: 10, // meters between updates
  ),
).listen((position) {
  setState(() => _current = position);
});

// in dispose():
subscription.cancel();

LocationAccuracy.medium is the right default for most UI — best/bestForNavigation draws noticeably more battery and is worth reserving for the moments precision genuinely matters.

2. Motion Sensors with sensors_plus

terminal
flutter pub add sensors_plus
reading the accelerometer
final subscription = accelerometerEventStream().listen((event) {
  final magnitude = sqrt(event.x * event.x + event.y * event.y + event.z * event.z);
  if (magnitude > 25) {
    onShakeDetected();
  }
});

// in dispose():
subscription.cancel();

sensors_plus exposes the same stream pattern for gyroscopeEventStream and magnetometerEventStream — each is a stream you subscribe to and must remember to cancel, or it keeps firing (and draining battery) after the screen is gone.

3. Requesting Permissions Properly

Both platforms show the OS permission dialog once per genuine request — so a rationale screen before the first real request, explaining why the app wants location, is the difference between a considered "yes" and a reflexive "no."

a rationale screen before the OS prompt
class LocationRationale extends StatelessWidget {
  final VoidCallback onContinue;
  const LocationRationale({super.key, required this.onContinue});

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        const Text('We use your location to show nearby stores.'),
        const Text('We never share it, and you can turn it off anytime in Settings.'),
        ElevatedButton(onPressed: onContinue, child: const Text('Continue')),
      ],
    );
  }
}

"While using the app" covers location reads while the app is open and visible — this is almost always sufficient. "Always" (background location) is a separate, stricter permission both platforms review closely; only request it if a feature genuinely needs to track location while the app is fully backgrounded or closed.

4. Hands-on Exercise

Hands-on

Build a "find my spot" screen with a shake gesture

Combine location and motion sensors into one small, permission-respectful screen.

Requirements:

  1. A rationale screen shown before the first location permission request.
  2. A screen showing live latitude/longitude via getPositionStream, with the subscription correctly cancelled in dispose().
  3. An accelerometer listener detecting a shake gesture (magnitude over a threshold) that re-centers/refreshes the displayed location when triggered.
  4. A visible permission-denied state that doesn't crash or show blank coordinates if the user declines, distinguishing denied from deniedForever.
  5. Both subscriptions (location and accelerometer) confirmed removed on unmount by navigating away and checking no further console output appears from either listener.
Hint

If the shake gesture never fires (or fires constantly) on a real device, your magnitude threshold needs tuning — real device shakes typically read well above normal gravity's ~9.8 m/s² baseline on each axis combined, so a threshold well above that (around 20-25 on the summed magnitude) is a reasonable starting point; the emulator/simulator won't produce real motion data to test against at all.

5. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does the geolocator example check isLocationServiceEnabled() separately from checking permission?

They're two independent conditions — permission is about whether this specific app is allowed to access location, while the device's location service being enabled at all (the system-wide GPS/location toggle) is a separate setting entirely. An app can have full permission granted while location services are globally off, and checking only permission would miss that case entirely.

Q2

What is the practical difference between LocationPermission.denied and LocationPermission.deniedForever, and why does it matter for how the app responds?

denied means the user hasn't decided yet or declined without permanently blocking future prompts — requesting again is reasonable. deniedForever means the OS will no longer show the permission dialog at all for this app; the only way forward is directing the user to the system Settings app manually, so the app's UI needs to handle that case differently (with a link to settings) rather than futilely calling requestPermission() again.

Q3

Why would a feature that only shows nearby stores while the app is open need just "while using the app" permission, not "always"?

The feature only needs location data at moments the user is actively looking at the app — there's no requirement to track location while the app is backgrounded or closed. "Always" permission is reserved for features that genuinely need background tracking (a fitness app logging a route, a delivery app tracking a driver), and requesting it unnecessarily draws more scrutiny from both platforms and from users.

Q4

Why does an accelerometer listener need its subscription cancelled in dispose(), given the sensor itself doesn't hold a lock the way a camera does?

Even without a hardware lock, an uncancelled stream subscription keeps its callback firing and consuming CPU/battery for a screen that's no longer visible or relevant — and it can also reference state or context from an already-disposed widget, which risks calling setState on a widget that no longer exists. Cancelling in dispose() is what actually stops the listener rather than leaving it running invisibly in the background.