{"id":"l-rn-4-4","courseId":"react-native-apps","lang":"en","kind":"lesson","summary":"Track a hike’s route with expo-location, attach trail photos with expo-image-picker, and schedule a reminder with expo-notifications, cleaning up subscriptions and filtering noisy GPS data.","html":"<p>Three device features make Trailhead more than a list: the route you walked, the photos you took, and a nudge at the end of the day to log the hike you forgot. They use three Expo modules with a lot in common: each needs a permission (asked the way the last lesson showed), each returns data you’ll store with the tools from two lessons ago, and each has one detail that bites if you skip it.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Tracking a route</h2>\n<p><code dir=\"ltr\">getCurrentPositionAsync</code> gives you one reading, which is enough to tag where a hike started. Tracking needs a stream: <code dir=\"ltr\">watchPositionAsync</code> calls your function with each new position and returns a <strong>subscription</strong> that keeps the GPS running until you remove it.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/hooks/useRoute.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Location </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useEffect, useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Point</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">latitude</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">longitude</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">accuracy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">timestamp</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useRoute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">active</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">points</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setPoints</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Point</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]>([]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">active) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> subscription</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Location</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">LocationSubscription</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cancelled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    Location.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">watchPositionAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      { accuracy: Location.Accuracy.High, distanceInterval: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, timeInterval: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">coords</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">timestamp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">        setPoints</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">prev</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">prev, { latitude: coords.latitude, longitude: coords.longitude, accuracy: coords.accuracy, timestamp }]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">then</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">sub</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (cancelled) sub.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">remove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      else</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> subscription </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sub;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    });</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      cancelled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      subscription?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">remove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [active]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> points;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The <code dir=\"ltr\">cancelled</code> flag handles a subtle race: the subscription arrives asynchronously, so if the screen unmounts before the promise resolves, the cleanup has nothing to remove yet. The flag makes the late subscription remove itself.</p>\n<p>Appending to an array in state on every reading is fine for a few thousand points, which covers a long day hike. For multi-day tracks, write points to SQLite in batches and keep only the recent ones in memory, so a single state update never copies tens of thousands of objects.</p>\n<p>The options are a battery budget. <code dir=\"ltr\">Accuracy.High</code> asks for a precision of about ten meters; <code dir=\"ltr\">Balanced</code> is about a hundred meters and much cheaper. <code dir=\"ltr\">distanceInterval: 10</code> means “only call me after moving ten meters”, and <code dir=\"ltr\">timeInterval</code> caps the frequency on Android. A hiking track needs high accuracy but not a point every second.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Trusting every GPS reading</span></p>\n<p>Under trees, in canyons and next to cliffs, GPS readings wander. Each <code dir=\"ltr\">coords.accuracy</code> is the radius of uncertainty in meters, and a single 80 m reading can add hundreds of meters of fake distance as the route zigzags. Drop readings above a threshold (25 m works well for walking) before computing distance or drawing the line. That’s this lesson’s exercise.</p>\n</aside>\n<p>Tracking with the screen locked needs background location and a task registered with <code dir=\"ltr\">expo-task-manager</code>, plus the extra permission and review scrutiny from the last lesson. Ship foreground tracking first; many hikers keep the screen on to see the map anyway.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Trail photos</h2>\n<p>For photos, <code dir=\"ltr\">expo-image-picker</code> opens the system’s own photo library or camera UI, which is familiar to users and handles editing for you:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-image-picker'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> addPhoto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">fromCamera</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ImagePicker</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ImagePickerOptions</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { mediaTypes: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'images'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], quality: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.7</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, allowsEditing: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (fromCamera) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> permission</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">requestCameraPermissionsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">permission.granted) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> fromCamera</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ?</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">launchCameraAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(options)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    :</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">launchImageLibraryAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(options);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (result.canceled) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> uri</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> result.assets[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].uri;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // copy into the app's document directory, then save the new path on the hike</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">quality: 0.7</code> compresses the JPEG, which matters when a hike has twenty photos. Cancelling isn’t an error: the promise resolves with <code dir=\"ltr\">canceled: true</code>. And the returned <code dir=\"ltr\">uri</code> points into a cache directory the system may clear, so copy the file into your app’s document directory with <code dir=\"ltr\">expo-file-system</code> and store that path in SQLite, as the storage lesson described. The iOS Simulator has no camera, so test camera capture on a real phone.</p>\n<p>When you need your own camera interface, such as scanning QR codes on trail markers, use <code dir=\"ltr\">expo-camera</code>, whose <code dir=\"ltr\">CameraView</code> component renders a live preview inside your layout.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">A reminder notification</h2>\n<p>Hikers forget to log hikes. A daily reminder at 7 pm, only on days they’ve turned it on, is a local notification: scheduled on the device, no server involved.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/lib/reminders.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-notifications'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Platform } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setNotificationHandler</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  handleNotification</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldShowBanner: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldShowList: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldPlaySound: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldSetBadge: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> enableDailyReminder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (Platform.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">OS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'android'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setNotificationChannelAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'reminders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Hike reminders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      importance: Notifications.AndroidImportance.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">DEFAULT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">granted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">requestPermissionsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">granted) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">cancelAllScheduledNotificationsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">scheduleNotificationAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    content: { title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Out on a trail today?'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, body: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log it while you remember the details.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data: { url: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/new-hike'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    trigger: { type: Notifications.SchedulableTriggerInputTypes.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">DAILY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, hour: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">19</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, minute: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, channelId: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'reminders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p><code dir=\"ltr\">setNotificationHandler</code> decides whether a notification shows while the app is open. On Android, notifications belong to <strong>channels</strong> that users can mute individually in Settings, so create a clearly named one. Cancelling before scheduling avoids stacking duplicate reminders each time the setting is toggled.</p>\n<p>The <code dir=\"ltr\">data.url</code> pays off with Expo Router: in the root layout, listen with <code dir=\"ltr\">Notifications.addNotificationResponseReceivedListener</code>, read <code dir=\"ltr\">response.notification.request.content.data.url</code>, and call <code dir=\"ltr\">router.push(url)</code>. Tapping the reminder then opens the add-hike modal directly, through the same routes your deep links use.</p>\n<p><strong>Push</strong> notifications, sent from a server, need more: a development build, credentials for Apple’s and Google’s push services (EAS manages them), and a push token sent to your backend. The scheduling and handling code you just wrote stays the same.</p>\n<p>Section 5 is about getting all of this into users’ hands: debugging, testing and shipping.</p>\n","toc":[{"id":"sec-1","text":"Tracking a route","level":2},{"id":"sec-2","text":"Trail photos","level":2},{"id":"sec-3","text":"A reminder notification","level":2}],"takeaways":["<code dir=\"ltr\">watchPositionAsync</code> streams positions until you call <code dir=\"ltr\">remove()</code> on the subscription it returns, so remove it in the effect’s cleanup.","Accuracy and update intervals trade precision for battery; choose the least precise settings the feature can live with.","GPS readings are noisy, so drop points with poor accuracy before computing distance.","expo-image-picker returns <code dir=\"ltr\">canceled</code> plus an <code dir=\"ltr\">assets</code> array; picked files start in a cache, so copy anything you keep.","Local notifications are scheduled on the device with a trigger, and <code dir=\"ltr\">data</code> can carry the route to open when the user taps them."],"further":[{"title":"Location","url":"https://docs.expo.dev/versions/latest/sdk/location/"},{"title":"ImagePicker","url":"https://docs.expo.dev/versions/latest/sdk/imagepicker/"},{"title":"Notifications","url":"https://docs.expo.dev/versions/latest/sdk/notifications/"}],"quiz":[{"q":"<p>Trailhead’s tracking screen calls <code dir=\"ltr\">watchPositionAsync</code> in an effect but never stores the result. What happens after the user leaves the screen?</p>","options":[{"text":"The subscription stops automatically when the component unmounts.","why":"Native subscriptions don’t know about your component. Only <code dir=\"ltr\">remove()</code> stops them."},{"text":"The app crashes the next time a position arrives.","why":"The callback still runs; React just ignores state updates for the unmounted screen. It’s a silent leak, not a crash."},{"text":"Nothing, because location only updates while the screen is visible.","why":"Foreground location keeps updating while the app is open, whatever screen is showing."},{"text":"GPS keeps running and draining the battery, because nothing called <code dir=\"ltr\">remove()</code> on the subscription.","why":"Correct. Store the subscription and call <code dir=\"ltr\">subscription.remove()</code> in the effect’s cleanup."}],"answer":3},{"q":"<p>A recorded track jumps 400 m sideways for one point while the hiker stood still under trees. What’s the best response in code?</p>","options":[{"text":"Request <code dir=\"ltr\">Accuracy.BestForNavigation</code> so the GPS never makes mistakes.","why":"Higher accuracy settings reduce error but can’t eliminate it, and they cost more battery."},{"text":"Discard points whose reported <code dir=\"ltr\">coords.accuracy</code> is worse than a threshold, such as 25 m, before adding them to the route.","why":"Correct. Each reading reports its own uncertainty radius; filtering out the bad ones keeps the route and the distance honest."},{"text":"Round all coordinates to two decimal places.","why":"Two decimal places of latitude is about 1 km of precision, which destroys the route."}],"answer":1},{"q":"<p>After <code dir=\"ltr\">launchImageLibraryAsync</code>, the user backs out without choosing. What does your code see?</p>","options":[{"text":"The promise rejects with a “cancelled” error.","why":"Cancelling isn’t an error. The promise resolves normally."},{"text":"The promise never resolves.","why":"It resolves as soon as the picker closes, whatever the user did."},{"text":"A result with <code dir=\"ltr\">canceled: true</code> and no assets, which you should handle by doing nothing.","why":"Correct. Check <code dir=\"ltr\">if (result.canceled) return;</code> before reading <code dir=\"ltr\">result.assets[0].uri</code>."}],"answer":2}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Total distance of a recorded route in km, rounded to 2 decimals.\n// Ignore points whose accuracy (meters) is worse than maxAccuracyM.\nfunction trackDistanceKm(points, maxAccuracyM = 25) {\n  let total = 0;\n  for (let i = 1; i < points.length; i++) {\n    const dLat = points[i].latitude - points[i - 1].latitude;\n    const dLon = points[i].longitude - points[i - 1].longitude;\n    total += Math.sqrt(dLat * dLat + dLon * dLon);\n  }\n  return total;\n}\n\nconst route = [\n  { latitude: 46.5, longitude: 7.9, accuracy: 8 },\n  { latitude: 46.51, longitude: 7.9, accuracy: 12 },\n  { latitude: 46.53, longitude: 7.95, accuracy: 90 },\n  { latitude: 46.51, longitude: 7.92, accuracy: 10 },\n];\nconsole.log(trackDistanceKm(route), 'km');\n"},"solution":{"js":"const EARTH_RADIUS_KM = 6371;\nconst toRad = (deg) => (deg * Math.PI) / 180;\n\nfunction haversineKm(a, b) {\n  const dLat = toRad(b.latitude - a.latitude);\n  const dLon = toRad(b.longitude - a.longitude);\n  const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a.latitude)) * Math.cos(toRad(b.latitude)) * Math.sin(dLon / 2) ** 2;\n  return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(h));\n}\n\nfunction trackDistanceKm(points, maxAccuracyM = 25) {\n  const good = points.filter((p) => p.accuracy == null || p.accuracy <= maxAccuracyM);\n  let total = 0;\n  for (let i = 1; i < good.length; i++) total += haversineKm(good[i - 1], good[i]);\n  return Math.round(total * 100) / 100;\n}\n\nconst route = [\n  { latitude: 46.5, longitude: 7.9, accuracy: 8 },\n  { latitude: 46.51, longitude: 7.9, accuracy: 12 },\n  { latitude: 46.53, longitude: 7.95, accuracy: 90 },\n  { latitude: 46.51, longitude: 7.92, accuracy: 10 },\n];\nconsole.log(trackDistanceKm(route), 'km');\n"},"checks":[{"id":"one-step","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return typeof trackDistanceKm === 'function' && trackDistanceKm([p(0, 0), p(0.01, 0)]) === 1.11;"},{"id":"sums-route","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(46.5, 7.9), p(46.51, 7.9), p(46.51, 7.92)]) === 2.64;"},{"id":"drops-inaccurate","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(46.5, 7.9, 8), p(46.51, 7.9, 12), p(46.53, 7.95, 90), p(46.51, 7.92, 10)]) === 2.64;"},{"id":"custom-threshold","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(0, 0, 5), p(0.01, 0, 40), p(0.02, 0, 5)], 50) === 2.22 && trackDistanceKm([p(0, 0, 5), p(0.01, 0, 40), p(0.02, 0, 5)], 30) === 2.22;"},{"id":"short-input","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([]) === 0 && trackDistanceKm([p(1, 1)]) === 0 && trackDistanceKm([p(1, 1, 99), p(1.01, 1, 99)]) === 0;"},{"id":"null-accuracy","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(0, 0, null), p(0.01, 0, null)]) === 1.11;"}],"title":"Measure a GPS track","prompt":"<p>Fix <code dir=\"ltr\">trackDistanceKm(points, maxAccuracyM = 25)</code> so Trailhead shows the real length of a recorded hike. Each point has <code dir=\"ltr\">latitude</code>, <code dir=\"ltr\">longitude</code> (degrees) and <code dir=\"ltr\">accuracy</code> (meters, or <code dir=\"ltr\">null</code> when unknown).</p>\n<ul>\n<li>Ignore points whose <code dir=\"ltr\">accuracy</code> is <strong>greater than</strong> <code dir=\"ltr\">maxAccuracyM</code>. Keep points with <code dir=\"ltr\">null</code> accuracy.</li>\n<li>Add up the great-circle distance between each remaining point and the next, using the haversine formula with an Earth radius of <strong>6371 km</strong>.</li>\n<li>Return kilometers rounded to <strong>2 decimals</strong>. Fewer than two usable points means <code dir=\"ltr\">0</code>.</li>\n</ul>\n<p>The starter treats degrees as flat coordinates, which is wrong everywhere and badly wrong far from the equator.</p>\n","hints":["Filter first: <code dir=\"ltr\">points.filter((p) =&gt; p.accuracy == null || p.accuracy &lt;= maxAccuracyM)</code>.","Haversine: convert the latitude and longitude differences to radians, then <code dir=\"ltr\">h = sin²(dLat/2) + cos(lat1)·cos(lat2)·sin²(dLon/2)</code> and <code dir=\"ltr\">distance = 2 · 6371 · asin(√h)</code>.","Round once at the end with <code dir=\"ltr\">Math.round(total * 100) / 100</code>, not after each step, or the rounding errors add up."],"explanation":"<p>Degrees aren’t a flat grid: one degree of longitude shrinks from about 111 km at the equator to zero at the poles, so the starter’s Pythagoras on raw degrees gave nonsense. The haversine formula measures along the Earth’s surface, which is accurate to well within a GPS reading’s own error for hiking distances. Filtering by <code dir=\"ltr\">accuracy</code> before summing removes the zigzags that otherwise add fake kilometers under trees. The function is pure, so the tracking screen can call it on every new point, or you can compute it once when the hike is saved.</p>\n","checkLabels":{"one-step":"One step of 0.01° latitude is about 1.11 km","sums-route":"Distances along a route are added up","drops-inaccurate":"Points with poor accuracy are ignored","custom-threshold":"The accuracy threshold can be changed","short-input":"Fewer than two usable points gives 0","null-accuracy":"Points with unknown (null) accuracy are kept"}},"words":658,"readMinutes":3}