iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
佛心分享-SideProject30

30 天開發一款真正能每天使用的散步 App系列 第 20

離線模式(1)——SQLite 解決不了真正的離線問題

  • 分享至 

  • xImage
  •  

目前 SQLite 在散步功能中的角色,主要是保存散步過程中的本地資料。

GPS 更新後會先寫入 SQLite,因此即使暫時沒有網路,也能持續記錄定位。

但離線模式真正要解決的,其實不只是「GPS 有沒有保存下來」。

散步功能還包含建立散步、暫停、繼續、結束散步,以及最後的正式統計。如果這些操作都依賴後端 API,那麼即使 GPS 已經保存到 SQLite,使用者仍然可能因為斷網而無法正常完成一場散步。

所以我希望做到的是:

即使沒有網路,使用者仍然可以正常開始、進行、暫停、恢復與結束散步。

等網路恢復後,再把這段期間累積的資料同步到後端。

也就是說,目前 SQLite 解決的是資料保存,但還沒有真正解決「散步流程如何在離線狀態下繼續運作」。

因此這次重新調整的不只是資料表,而是整個 SQLite 在離線架構中的定位。

SQLite 保存的是散步進行中的本地狀態

SQLite 並不是單純用來保存 GPS,而是同時保存「散步進行中的本地狀態」以及「離線查看所需的歷史摘要」。

散步進行期間,需要持續維護的資料其實不只有 GPS,還包含散步主資料、暫停狀態,以及推薦路線導航狀態。

它們雖然都屬於同一場散步,但生命週期卻完全不同。

  • GPS 幾乎每幾秒就會新增一筆。
  • Pause 只有暫停或恢復時才會更新。
  • Navigation State 則會隨著導航進度持續變化。

如果全部放在同一筆資料,每次收到 GPS 都必須重新更新其他不相關的內容,不但增加寫入成本,也讓不同職責彼此耦合。

我最後將它們拆成不同資料表:

  1. walks:散步主資料
  2. walk_points:GPS 定位點
  3. local_walk_pauses:暫停紀錄
  4. walk_navigation_state:推薦路線導航狀態

每張資料表只負責自己的生命週期,也讓散步恢復、資料同步與後續維護都變得更單純。

這樣一來,即使使用者走到沒有網路的地方,App 仍然可以從 SQLite 取得散步狀態,繼續記錄 GPS、計算距離、處理暫停與恢復,而不是因為 API 暫時無法使用就中斷散步流程。

離線時記錄頁也不應該依賴網路

既然散步資料已經保存在本地,那麼離線時,其他功能也不能全部要求重新向後端取得資料。

例如使用者在沒有網路的情況下打開散步記錄頁,應該仍然可以看到已經同步到本地的散步摘要:

  • 日期
  • 距離
  • 散步時間
  • 平均速度
  • 其他正式統計

這些資料可以直接從本地的 local_walk_history 取得。

但完整 GPS 軌跡則不一定會永久保存在本地。

我的設計是,散步完成並同步成功後,只保留歷史紀錄需要的摘要,清除這次散步產生的暫存 GPS 與其他進行中資料。

如果使用者離線查看一場已經完成的舊散步,可以正常看到摘要;如果需要查看完整軌跡,而本地已經沒有保存,就等恢復網路後再從後端取得。

這樣可以讓離線記錄頁保持基本可用,同時避免 SQLite 永久保存大量 GPS 點位。

同步完成後,為什麼只保留摘要?

另一個問題是:「同步完成後,本地到底還要留下什麼?」

很多離線 App 會選擇把完整 GPS 永久保存在 SQLite,但我最後沒有採用這種方式。

原因是完整 GPS 在散步完成後,其實很少再被使用。例如歷史列表,只需要距離、時間、平均速度等摘要資訊,就能完成大部分功能。

如果每次開啟歷史紀錄,都還需要讀取數百甚至上千筆 GPS 點位,不但增加查詢成本,也會讓 SQLite 持續保存大量幾乎不會再修改的資料。

另一方面,正式統計已經由後端重新計算完成,完整軌跡也已經同步到後端,因此本地再保存一份完整 GPS,意義並不大。

因此,只有在後端確認散步已經完成,而且正式結果也成功保存到本地後,才會清除這次散步產生的暫存資料:

  • walk_points
  • local_walk_pauses
  • walk_navigation_state
  • walks
  • 已完成的同步工作

真正需要查看完整軌跡時,再向後端取得即可。

SQLite 的責任,是支撐散步進行中的離線流程,以及提供離線狀態下仍然需要的本地資料,而不是維護永久的完整 GPS 歷史。

Transaction 解決的是資料一致性

定位更新看起來只是新增一筆 GPS,但對 SQLite 而言,它其實是一系列相關資料的更新。

例如:

  • 新增 GPS 點
  • 更新 Point Sequence
  • 更新 Walk 的點位數量

這些資料彼此都有關聯。

如果其中一步成功、另一步失敗,就可能留下不完整的狀態。例如 GPS 已經存在,但 Point Sequence 沒有更新,之後恢復散步時,就可能造成點位排序錯誤。

因此我把相關更新放進同一個 Transaction:

await database.transaction(async tx => {
  await walkPointRepository.insert(tx, point);

  await walkRepository.update(tx, {
    nextPointSequence: nextSequence,
  });
});

Transaction 在這裡不是為了提升效能,而是讓一次定位更新成為不可分割的操作。

只要其中一步失敗,整個更新就會一起回滾,確保 SQLite 始終維持一致的狀態。

推薦路線為什麼不是只存 Polyline?

推薦路線建立時,需要透過 Google Places 與 Google Routes API,因此這個階段需要網路。

不過真正開始散步後,我並不希望每次都依賴 Google API 或重新解析 Polyline。

因為完成率計算、偏離路線判斷,都需要頻繁取得路線上的座標。

如果每次開始散步才重新解析 encodedPolyline,不但增加額外計算,也讓所有路線功能都依賴同一份字串。

因此我在建立路線時,就直接把路線拆成不同層級的資料保存到 SQLite。

  1. generated_routes
    • 路線基本資訊
    • encodedPolyline
    • 預估距離
    • 預估時間
    • 使用者設定
  2. generated_route_points
    • sequence
    • latitude
    • longitude
    • cumulativeDistanceMeters
  3. route_waypoints
    • placeId
    • name
    • category

其中最重要的是 generated_route_points

完成率、偏離判斷等功能,都直接使用這張資料表,而不是每次重新解析 Polyline。

也就是說,Google API 負責建立推薦路線;路線建立完成後,散步過程中的導航則使用本地保存的 Route Points。

這樣即使散步途中失去網路,完成率計算與偏離路線判斷仍然可以正常進行,不需要再次呼叫 Google API。


上一篇
散步越久地圖越卡?GPS 軌跡的效能優化
下一篇
離線模式(2)——安全同步本地資料
系列文
30 天開發一款真正能每天使用的散步 App22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言