Week 8: Location, Sensors & Permissions

GPS coordinates and motion data are two more sensors an app can read directly from the device — and both sit behind a runtime permission prompt that, done badly, is the single biggest reason users bounce off an onboarding flow. This week reads both sensors properly, and gets the permission request itself right.

Phase 4 of 8 Week 8 of 22 ~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 expo-location
  • Read accelerometer and gyroscope data with expo-sensors
  • Request foreground location permission with a clear rationale, and know when background permission is a separate ask
  • Build a small feature — live coordinates plus a shake gesture — from both sensors

1. Reading Location with expo-location

terminal
npx expo install expo-location
getting a one-off location
import * as Location from 'expo-location';

async function getCurrentLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  if (status !== 'granted') return null;

  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.Balanced,
  });
  return location.coords; // { latitude, longitude, altitude, accuracy, ... }
}
watching location as it changes
useEffect(() => {
  let subscription: Location.LocationSubscription | undefined;

  (async () => {
    subscription = await Location.watchPositionAsync(
      { accuracy: Location.Accuracy.Balanced, timeInterval: 5000, distanceInterval: 10 },
      (loc) => setCoords(loc.coords)
    );
  })();

  return () => subscription?.remove();
}, []);

Accuracy.Balanced is the right default for most UI — Highest drains battery noticeably faster and is worth reserving for the moments accuracy genuinely matters, like turn-by-turn navigation.

2. Motion Sensors with expo-sensors

terminal
npx expo install expo-sensors
reading the accelerometer
import { Accelerometer } from 'expo-sensors';

useEffect(() => {
  Accelerometer.setUpdateInterval(100); // ms

  const subscription = Accelerometer.addListener(({ x, y, z }) => {
    const magnitude = Math.sqrt(x * x + y * y + z * z);
    if (magnitude > 2.5) {
      onShakeDetected();
    }
  });

  return () => subscription.remove();
}, []);

expo-sensors exposes the same listener pattern for Gyroscope, Magnetometer and Barometer — each is a stream of readings you subscribe to and must remember to unsubscribe from, or it keeps firing (and draining battery) after the screen unmounts.

3. Requesting Permissions Properly

Both platforms show the OS permission dialog exactly once per reason given unless the user changes it manually in Settings — so the moment you call requestForegroundPermissionsAsync() for the first time is the only real shot at a "yes." Show your own explanation screen first.

a rationale screen before the OS prompt
function LocationRationale({ onContinue }: { onContinue: () => void }) {
  return (
    <View>
      <Text>We use your location to show nearby stores.</Text>
      <Text>We never share it, and you can turn it off anytime in Settings.</Text>
      <Pressable onPress={onContinue}>
        <Text>Continue</Text>
      </Pressable>
    </View>
  );
}

Foreground permission (requestForegroundPermissionsAsync) covers location reads while the app is open and visible. Background location (requestBackgroundPermissionsAsync) is a separate, stricter request — both platforms require the foreground permission to already be granted first, and Apple in particular reviews background location use closely; only ask for it if the feature genuinely needs to track location while the app isn't open.

4. Hands-on Exercise

Hands-on

Build a "find my spot" screen with a shake-to-refresh gesture

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

Requirements:

  1. A rationale screen shown before the first location permission request, explaining why the app wants it.
  2. A screen showing live latitude/longitude, updating via watchPositionAsync, cleaned up correctly on unmount.
  3. An accelerometer listener that detects a shake gesture (magnitude over a threshold) and 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.
  5. Both subscriptions (location and accelerometer) removed in a cleanup function so they stop firing when the screen unmounts.
Hint

If the shake gesture fires constantly (or never), the accelerometer's update interval and your magnitude threshold need tuning together — a 100ms interval with a threshold around 2.5–3.0 on Math.sqrt(x*x+y*y+z*z) is a reasonable starting point on a real device (the simulator/emulator won't produce real motion data).

5. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why request location permission behind your own rationale screen rather than calling requestForegroundPermissionsAsync() the moment a screen mounts?

The OS dialog only shows once per permission unless the user manually resets it in Settings — so an unexplained prompt on first load, with no context, is likely to get a reflexive "Don't Allow" that's then hard to recover from. A brief explanation first gives the user a reason to say yes on the one attempt that matters.

Q2

Why does background location require a separate permission request from foreground location, on both platforms?

Background location — the app reading position while not open or visible — is more sensitive and more heavily scrutinized (especially by Apple's App Store review), so both platforms gate it behind its own explicit request, on top of foreground permission already being granted. It should only be requested when a feature genuinely needs it.

Q3

What happens if an accelerometer listener isn't removed when its screen unmounts?

The listener keeps firing in the background, continuing to consume CPU and battery for a screen the user can no longer see, and can trigger stale callbacks (like onShakeDetected) referencing state or navigation from an unmounted component. The cleanup function returned from useEffect exists specifically to call subscription.remove() and prevent this.

Q4

Why is Accuracy.Balanced a better default than Accuracy.Highest for most location UI?

Highest accuracy uses more power-hungry positioning methods and updates more aggressively, which measurably drains battery faster for a benefit most UI (showing a city or neighborhood, nearby results) doesn't need. Reserve Highest for features where precision genuinely matters, like turn-by-turn navigation.