目前 SQLite 在散步功能中的角色,主要是保存散步過程中的本地資料。
GPS 更新後會先寫入 SQLite,因此即使暫時沒有網路,也能持續記錄定位。
但離線模式真正要解決的,其實不只是「GPS 有沒有保存下來」。
散步功能還包含建立散步、暫停、繼續、結束散步,以及最後的正式統計。如果這些操作都依賴後端 API,那麼即使 GPS 已經保存到 SQLite,使用者仍然可能因為斷網而無法正常完成一場散步。
所以我希望做到的是:
即使沒有網路,使用者仍然可以正常開始、進行、暫停、恢復與結束散步。
等網路恢復後,再把這段期間累積的資料同步到後端。
也就是說,目前 SQLite 解決的是資料保存,但還沒有真正解決「散步流程如何在離線狀態下繼續運作」。
因此這次重新調整的不只是資料表,而是整個 SQLite 在離線架構中的定位。
SQLite 並不是單純用來保存 GPS,而是同時保存「散步進行中的本地狀態」以及「離線查看所需的歷史摘要」。
散步進行期間,需要持續維護的資料其實不只有 GPS,還包含散步主資料、暫停狀態,以及推薦路線導航狀態。
它們雖然都屬於同一場散步,但生命週期卻完全不同。
如果全部放在同一筆資料,每次收到 GPS 都必須重新更新其他不相關的內容,不但增加寫入成本,也讓不同職責彼此耦合。
我最後將它們拆成不同資料表:
walks:散步主資料walk_points:GPS 定位點local_walk_pauses:暫停紀錄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 歷史。
定位更新看起來只是新增一筆 GPS,但對 SQLite 而言,它其實是一系列相關資料的更新。
例如:
這些資料彼此都有關聯。
如果其中一步成功、另一步失敗,就可能留下不完整的狀態。例如 GPS 已經存在,但 Point Sequence 沒有更新,之後恢復散步時,就可能造成點位排序錯誤。
因此我把相關更新放進同一個 Transaction:
await database.transaction(async tx => {
await walkPointRepository.insert(tx, point);
await walkRepository.update(tx, {
nextPointSequence: nextSequence,
});
});
Transaction 在這裡不是為了提升效能,而是讓一次定位更新成為不可分割的操作。
只要其中一步失敗,整個更新就會一起回滾,確保 SQLite 始終維持一致的狀態。
推薦路線建立時,需要透過 Google Places 與 Google Routes API,因此這個階段需要網路。
不過真正開始散步後,我並不希望每次都依賴 Google API 或重新解析 Polyline。
因為完成率計算、偏離路線判斷,都需要頻繁取得路線上的座標。
如果每次開始散步才重新解析 encodedPolyline,不但增加額外計算,也讓所有路線功能都依賴同一份字串。
因此我在建立路線時,就直接把路線拆成不同層級的資料保存到 SQLite。
generated_routes
encodedPolyline
generated_route_points
sequence
latitude
longitude
cumulativeDistanceMeters
route_waypoints
placeId
name
category
其中最重要的是 generated_route_points。
完成率、偏離判斷等功能,都直接使用這張資料表,而不是每次重新解析 Polyline。
也就是說,Google API 負責建立推薦路線;路線建立完成後,散步過程中的導航則使用本地保存的 Route Points。
這樣即使散步途中失去網路,完成率計算與偏離路線判斷仍然可以正常進行,不需要再次呼叫 Google API。