iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0

終於要開始實作功能了,這個 Project 最重要也最基礎的功能就是定位,如果無法獲取定位或者定位不穩定,那後面的路線紀錄、距離計算、速度分析與背景定位都無法繼續實作。

今天只會先完成在打開 App 的情況下持續取得位置,並讓地圖跟著使用者移動。

設計定位流程

React Native 本身沒有提供完整的定位追蹤流程,因此通常會透過第三方定位套件,為了避免定位邏輯散落在各個 UI 中,我會建立一個 LocationService,統一負責:

  • 請求與檢查定位權限
  • 啟動持續定位
  • 接收 Android 與 iOS 回傳的位置
  • 將原始資料轉換成 App 使用的格式
  • 停止定位與清除監聽

如果 UI 直接依賴某個定位套件,之後要更換套件、加入背景定位,畫面邏輯也會一起受到影響。

目前簡易的定位流程如下:

  1. 請求定位權限
  2. 啟動 LocationService
  3. 持續取得目前位置
  4. 更新使用者位置

把定位邏輯抽離畫面

透過 LocationService 分離邏輯和UI後,畫面只需要知道兩件事:

  • 什麼時候開始定位
  • 什麼時候停止定位

至於底層使用哪個套件、Android 與 iOS 如何取得位置,都由 Service 負責。

介面大致如下:

export interface LocationService {
  startTracking(
    onLocation: (location: LocationPoint) => void,
    onError: (error: LocationError) => void,
  ): Promise<void>;

  stopTracking(): void;
}

畫面只需要呼叫這個介面:

await locationService.startTracking(
  location => {
    updateCurrentLocation(location);
  },
  error => {
    handleLocationError(error);
  },
);

這樣之後更換底層定位實作時,不需要跟著修改UI。

定位權限與定位狀態是兩件事

實作定位流程時,很容易把「系統權限」和「定位狀態」混在一起,但這兩者其實是不同的狀態。

系統權限

系統權限代表 App 是否有資格讀取位置,Android 和 iOS 都可以大概抽象成:

Not Determined
      ↓
Requesting
      ↓
Granted / Denied / Blocked

權限一旦允許,不會因為停止定位就回到未授權狀態。

定位狀態

定位狀態則代表 App 目前是否正在取得位置,由APP管理

例如:

 Idle
   │
   ▼
Locating
   │
   ▼
Tracking
   │
   ▼
Stopping
   │
   ▼
 Idle

每次開始與停止定位時,這段流程都會重新執行。

完整流程大致如下:

       [ 初始狀態 / Idle ]
               │
               ▼
        Not_Determined
               │
               ▼
      Requesting_Permission ──( 使用者拒絕 )──> [ Permission_Denied ]
               │                                       │
         ( 使用者同意 )                                  ▼
               │                               ( 提示並引導至設定頁 )
               ▼
            Granted
               │
               ▼
           Locating ───────( GPS 訊號失敗 )─────> [ Location_Error ]
               │
               ▼
           Tracking ───────( 訊號中斷/關閉 )───> [ Location_Error ]
               │
               ▼
           Stopping
               │
               ▼
         [ 回到 Idle ]

處理永久拒絕權限

如果使用者只是第一次拒絕權限,App 還可以再次請求。

但如果權限狀態已經變成 blocked,系統通常不會再顯示權限視窗,只能引導使用者前往設定頁修改。

if (permissionStatus === 'blocked') {
  await Linking.openSettings();
}

因此,畫面不能只顯示「定位失敗」,還需要讓使用者知道要去哪裡重新開啟權限。

Android 定位權限

Android 需要使用定位功能需要宣告以下權限:

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

ACCESS_FINE_LOCATION 可以取得較精確的位置。

ACCESS_COARSE_LOCATION 則是較粗略的位置權限。新版 Android 允許使用者只提供大概位置,因此兩個權限都需要處理。

背景定位需要額外的權限與 Foreground Service,這部分明天再說。

iOS 定位權限

iOS 需要在 Info.plist 中說明為什麼 App 要取得位置:

<key>NSLocationWhenInUseUsageDescription</key>
<string>散步期間需要使用您的位置來顯示目前位置。</string>

NSLocationWhenInUseUsageDescription 描述文字必須明確說明「為什麼要用」以及「對使用者有什麼好處」,否則上架時容易被退回。

目前使用的是 When In Use 權限,只允許 App 位於前景時取得位置。

等到實作背景定位時,會需要加入 Always Authorization 與 Background Modes。

Google Maps Key 與設定

地圖使用 Google Maps,因此 Android 與 iOS 都需要在 Google Cloud Console 啟用對應的 SDK:

  • Maps SDK for Android
  • Maps SDK for iOS

兩個平台的 API Key 分開建立,並限制各自允許使用的 App。

Android Key 可以限制:

  • Package Name
  • SHA-1 Certificate Fingerprint

iOS Key 可以限制:

  • Bundle ID

這些設定在我之前的鐵人賽有提到過,可以看一下,這裡只處理 Key 如何放進專案。

Google 登入流程與 OAuth Client 設定

Android Google Maps Key

Android 的 Google Maps API Key 放在 apps/mobile/android/local.properties

例如:

GOOGLE_MAPS_API_KEY=xxxxxxxxxxxxxxx

接著在 android/app/build.gradle 讀取 local.properties 內的值:

defaultConfig {
    applicationId "com.tracewalk"
    minSdkVersion rootProject.ext.minSdkVersion
    targetSdkVersion rootProject.ext.targetSdkVersion
    versionCode 1
    versionName "1.0"
    manifestPlaceholders.googleMapsApiKey = localProperties.getProperty("GOOGLE_MAPS_API_KEY", "")
}

最後在 AndroidManifest.xml 中引用:

<application>
 <meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${googleMapsApiKey}" />
</application>

API Key 沒有直接寫進 AndroidManifest.xml,是為了避免將不同環境的 Key 寫死在專案裡面。

iOS Google Maps Key

iOS 透過 Xcode 建立 GOOGLE_MAPS_API_KEY User-Defined Setting,避免直接將 Key 寫死在程式碼內。

使用 Xcode 開啟 xcworkspace,選擇左側的 TARGETS,切換到 Build Settings,點擊左上角的 +,選擇 Add User-Defined Setting,並建立一個名稱為 GOOGLE_MAPS_API_KEY 的設定。

接著在 iOS 初始化 Google Maps SDK 時讀取這個值。

Debug 與 Release 可以使用不同的 key,只要在左側展開GOOGLE_MAPS_API_KEY就能看到 Debug 和 Release:

顯示目前位置

成功取得位置後,就可以在地圖上顯示目前位置。

如果使用 react-native-maps,可以開啟顯示使用者位置:

<MapView
  ref={mapRef}
  style={styles.map}
  showsUserLocation
  showsMyLocationButton={false}
/>

也可以使用 Marker 顯示位置:

{currentLocation && (
  <Marker
    coordinate={{
      latitude: currentLocation.latitude,
      longitude: currentLocation.longitude,
    }}
  />
)}

地圖中心自動跟隨

定位功能初始後地圖中心會保持在當前位置,但隨著散步一段時間後,可能會走出當前地圖可視的範圍,地圖卻不會自動重新調整地圖中心。

所以我採取了以下作法:

  1. 預設為只要有新的通過過濾的點位,地圖立刻移動到最新點位。
    mapRef.current?.animateCamera(
      {
        center: {
          latitude: location.latitude,
          longitude: location.longitude,
        },
      },
      {
        duration: 500,
      },
    );
    
  2. 一旦使用者用手指拖曳地圖、雙指縮放,需要停止自動跟隨。
  3. 當使用者手指放開 5 秒鐘之內都沒有再次操作,就會自動移回最新點位,重啟自動跟隨。

這樣既能讓地圖持續跟著使用者,也不會阻止使用者自行操作地圖。

小結

最基本的定位以及地圖顯示功能已經完成,明天會開始說明背景定位(Background Location)的實作方法。


上一篇
建立後端開發環境
系列文
30 天開發一款真正能每天使用的散步 App5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言