Skip to content

Latest commit

 

History

4 Commits

Folders and files

Repository files navigation

BusTimer

BusTimer 是使用 Native Flutter UI 製作的香港巴士實時到站時間 App,支援 Android 及 iOS。介面採用青綠亮色、Material 3、圓角卡片、淺色玻璃感與大量留白,所有路線、車站、座標及 ETA 均來自巴士公司或香港政府公開資料。

本專案沒有 WebView、沒有內置假路線、沒有模擬 ETA,也不需要 API key。

功能

  • 九巴、龍運及城巴路線搜尋,輸入時即時進行前綴配對
  • 路線方向、起點、終點及營運公司資料
  • 收藏路線及最多 20 筆最近搜尋,本機持久保存
  • 可從裝置相簿選擇全畫面背景,支援自由裁剪、螢幕比例、9:16、3:4 及 1:1
  • 自訂背景以原色鋪滿首頁及詳情頁,不加入全頁色彩遮罩;透明效果只套用在資料卡片及必要控制元件
  • 路線方向 Segmented Button 及左右滑動轉場
  • 全線車站、站號、官方座標及可用車費
  • 單一車站展開的手風琴列表,每站最多三班 ETA
  • 選擇路線後讀取 GPS,以目前位置和官方車站座標計算最近站並自動展開
  • 進入詳情頁及回到前景時立即更新,前景每 10 秒自動更新
  • ETA 首次沒有有效時間時自動向官方 API 重刷新一次
  • 官方座標原生繪製的路線示意圖,不使用地圖 SDK
  • Loading、Empty、Error、Retry、Offline、No ETA 狀態
  • App 內完整私隱政策、公開資料來源及非官方應用程式聲明
  • 只提供繁體中文及淺色模式

定位只在路線詳情頁前景進行單次讀取,不會在背景持續追蹤或保存使用者座標。若拒絕權限或關閉 GPS,仍可手動展開車站。自訂背景亦只在裝置內處理及保存,不會上傳到任何伺服器。

公開資料來源

九巴及龍運

資料集:九龍巴士及龍運巴士路線實時到站數據

實際使用的 API:

  • https://data.etabus.gov.hk/v1/transport/kmb/route/
  • https://data.etabus.gov.hk/v1/transport/kmb/stop
  • https://data.etabus.gov.hk/v1/transport/kmb/route-stop/{route}/{direction}/{service_type}
  • https://data.etabus.gov.hk/v1/transport/kmb/route-eta/{route}/{service_type}

龍運與九巴共用上述運輸署 ETA 端點。BusTimer 以運輸署路線資料中的 COMPANY_CODE=LWB 辨認龍運,不會以路線字首猜測公司。

城巴

資料集:城巴的實時抵站時間及相關資料

實際使用的 API:

  • https://rt.data.gov.hk/v2/transport/citybus/route/CTB
  • https://rt.data.gov.hk/v2/transport/citybus/route-stop/CTB/{route}/{direction}
  • https://rt.data.gov.hk/v2/transport/citybus/stop/{stop_id}
  • https://rt.data.gov.hk/v2/transport/citybus/eta/CTB/{stop_id}/{route}

城巴 ETA 以官方路線、車站 ID 及方向欄位配對。城巴的路線車站與 ETA 資料集偶爾會為同一車站提供不同 seq,因此不會以跨資料集站序刪除本來有效的官方 ETA;九巴及龍運仍會按其路線 ETA 格式驗證站序。

運輸署路線及車費資料

資料集:公共交通路線及車費資料

  • https://static.data.gov.hk/td/routes-fares-xml/ROUTE_BUS.xml

此檔案提供營運公司及全程成人現金車費。全程票價只會顯示於方向起點;其他車站若無法從公開 API 可靠地對應分段收費,畫面顯示 車費 --,不會用全程票價或估算值冒充分段收費。

安裝

需求:

  • Flutter 3.44 或相容的 stable 版本
  • Dart 3.12 或以上
  • Android SDK 35/36、Java 17 或以上
  • iOS 開發需要完整 Xcode;Flutter 3.44 預設使用 Swift Package Manager
flutter pub get
dart run build_runner build
flutter run

Freezed 及 Json Serializable 產生檔已提交到專案;修改 Model 後才需要重新執行 build_runner。

Android Build

flutter doctor -v
flutter analyze
flutter test
flutter build apk --release

APK 輸出:

build/app/outputs/flutter-apk/app-release.apk

開發執行:

flutter run -d <android-device-id>

Android package ID 為 hk.bustimer.bustimer,主 Manifest 已加入 INTERNET、ACCESS_COARSE_LOCATION 及 ACCESS_FINE_LOCATION 權限。App 不申請背景定位;定位只用於找出所選路線最近的車站。相簿選圖使用 Android 系統選取器及 Scoped Storage,不需要額外儲存權限。

正式發佈簽名可建立不納入版本控制的 android/key.properties:

storeFile=/absolute/path/to/upload-keystore.jks
storePassword=...
keyAlias=...
keyPassword=...

Google Play 正式版本必須使用妥善備份的 upload key,並啟用 Play App Signing。已簽署 AAB 的輸出位置為:

build/app/outputs/bundle/release/app-release.aab

私隱政策公開網址:https://iskshadow195563.github.io/BusTimer/privacy/

Google Play 所需的商店說明、資料安全草稿、素材及提交清單位於 play_store/。upload key、android/key.properties 及密碼不得提交到版本控制。

iOS Build

首次使用:

sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -runFirstLaunch
flutter config --enable-swift-package-manager
flutter pub get

模擬器:

flutter run -d ios

不簽名建置檢查:

flutter build ios --no-codesign

正式封存請在 Xcode 開啟 ios/Runner.xcworkspace,設定 Apple Team、Bundle Identifier 及簽名後執行 Archive。專案已包含 FlutterGeneratedPluginSwiftPackage 及 Run Prepare Flutter Framework Script pre-action;Xcode 會自動解析 Connectivity 及 SharedPreferences 的 Swift packages。

iOS 的 Info.plist 已加入 NSLocationWhenInUseUsageDescription 及 NSPhotoLibraryUsageDescription;不需要 Always、背景定位或相機權限。

專案架構

lib/
├── core/
│   ├── error/          API 例外分類
│   ├── network/        Dio、timeout、retry、網絡檢查
│   ├── storage/        Hive 快取及 SharedPreferences
│   ├── theme/          Material 3 青綠亮色主題
│   └── utils/          ETA 格式化
├── data/
│   ├── models/         Freezed + Json Serializable Models
│   ├── services/       KMB、LWB、Citybus、運輸署 Services
│   └── repositories/   巴士及外觀 Repository 實作
├── domain/
│   └── repositories/   Repository contracts
├── presentation/
│   ├── providers/      Riverpod dependency injection
│   ├── viewmodels/     MVVM 狀態及操作
│   ├── screens/        首頁、詳情頁、背景裁剪及關於私隱頁
│   └── widgets/        路線卡、車站卡、狀態及路線示意圖
└── routing/            GoRouter

資料流:

Screen → ViewModel → BusRepository → Route / Stop / ETA Repository
                                  → Operator API Services

UI 不直接呼叫 API。BusRepository 是 Presentation 層唯一使用的巴士資料入口。

自訂背景邏輯

  1. 首頁右上角的背景按鈕開啟本機背景設定。
  2. 系統相簿只回傳使用者選擇的一張相片;App 不提供相機或網絡上傳。
  3. 相片長邊先限制至 4096px,修正 EXIF 方向並轉成 JPEG,避免大圖令裁剪頁記憶體過高。
  4. 裁剪頁可移動、縮放及選擇螢幕比例、9:16、3:4、1:1 或自由比例。
  5. 裁剪結果以最高 1440px 短邊及 2560px 長邊輸出;過小圖片只作高品質插值放大,不會生成額外圖像內容。
  6. 最終 JPEG quality 為 88,會移除 EXIF,並保存到 Application Support 目錄。SharedPreferences 只保存檔案路徑及裁剪比例。
  7. 新圖片成功寫入及保存偏好後才刪除舊圖片;取消、權限拒絕、損壞檔案或保存失敗均保留原背景。
  8. Android 若在相簿開啟期間回收 Activity,App 會透過 retrieveLostData() 恢復待裁剪圖片。

ETA 更新邏輯

  1. 詳情頁取得已快取或最新的官方車站列表及座標。
  2. 前景單次請求高準確度 GPS,使用球面距離計算最近車站,自動展開並捲動至該卡片。
  3. 若定位被拒絕、GPS 關閉或逾時,顯示明確狀態並暫時展開第一站;使用者仍可手動選站或重新定位。
  4. 立即請求所選車站 ETA;只接受可解析且未失效的官方時間。
  5. 首次回應沒有任何有效 ETA 時,立即再向同一官方 API 重刷新一次。
  6. 第二次仍沒有有效 ETA 時,顯示「官方暫未提供到站時間」,不顯示橫杠、不估算、不偽造。
  7. 使用者展開另一站時,上一站收起並請求新站 ETA。
  8. App 保持前景時,每 10 秒更新目前展開車站。
  9. App 進入 inactive、paused 或 detached 後取消 Timer。
  10. App 回到 resumed 後立即更新並重啟 Timer。
  11. ETA 不寫入 Hive 或 SharedPreferences;GPS 座標亦不保存。
  12. 週期請求期間保留目前 ETA,不先清空卡片;新舊 ETA 相同時保留原 List。各 ETA Widget 使用 Riverpod select 及 AnimatedSwitcher,只讓相關區域淡入淡出。
  13. 上一個 ETA 請求未完成時跳過該輪,避免 10 秒 Timer 產生併發重複請求。

ETA 顯示規則:

  • 61 秒以上:向上取整後顯示 N 分鐘
  • 60 秒內:即將到站
  • 官方沒有 ETA、空值或已明顯過期:自動重刷新一次,仍沒有時顯示明確的官方無資料狀態

BusTimer 每 10 秒重新向官方端點請求一次;若上游資料時間戳未改變,畫面會保留原 ETA List,不會為相同資料重繪整個車站列表。

快取策略

資料 儲存方式 有效期 失敗時行為
路線/方向 Hive CE + 記憶體 24 小時 使用過期快取並顯示離線狀態
路線車站/座標 Hive CE 24 小時 使用過期快取
收藏 SharedPreferences 永久 空集合
最近搜尋 SharedPreferences 永久,最多 20 筆 空列表
自訂背景路徑/裁剪比例 SharedPreferences + Application Support 圖片 直到使用者移除 圖片不存在時回復青綠漸層
ETA 不快取 不適用 空資料自動重拉一次,再顯示官方無資料狀態
使用者 GPS 不保存 不適用 顯示定位狀態,保留手動選站功能

同一路線、公司、方向、起終點若因官方 service_type 產生重複記錄,Repository 會優先保留主要服務類型;不同路線(例如 A41 與 A41P)及不同起終點仍會分開顯示。聯營路線按公司分開,避免混合不同站號與 ETA。

錯誤處理

ApiService 設定:

  • Connect timeout:10 秒
  • Receive timeout:25 秒
  • Send timeout:10 秒
  • 只對連線錯誤、timeout、HTTP 429 及 5xx 最多重試 3 次
  • 重試間隔:300ms、600ms

錯誤會分類為 offline、timeout、server、invalidResponse 或 unknown,ViewModel 再轉換成繁體中文狀態。

常見情況:

  • 一直顯示 Loading:先確認裝置可連接 data.etabus.gov.hk 及 rt.data.gov.hk;城巴服務偶爾回應較慢。
  • Offline:確認 Wi-Fi/流動數據;若有靜態快取仍可搜尋路線及查看車站。
  • No ETA:App 會自動再向官方 API 讀取一次;仍沒有有效時間時顯示「官方暫未提供到站時間」,不會補造班次。
  • 未能自動展開最近站:確認裝置 GPS 已開啟並允許精確定位;詳情頁的定位狀態卡可重新定位或開啟系統設定。
  • 未能選擇或裁剪背景:確認 iOS 相片權限;取消選圖不會清除目前背景,損壞或不支援的圖片會顯示中文錯誤。
  • 車費 --:代表公開資料無法可靠對應該站到終點的分段成人現金收費。
  • Build Runner 衝突:執行 dart run build_runner build,再執行 flutter analyze。
  • iOS 顯示 Application not configured:確認已安裝完整 Xcode,而不是只有 Command Line Tools,然後執行 sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer。

效能設計

  • ListView.builder/Sliver lazy list,不一次建立全線所有卡片
  • 搜尋只操作已正規化的記憶體路線列表,無 debounce、無網絡請求
  • Repository 合併重複呼叫,靜態資料使用記憶體及 Hive
  • Citybus 車站資料以有限並行數取得,避免一次發出數十個請求
  • 不下載遠端圖片;只有使用者主動選擇的本機背景會經壓縮後載入
  • 背景按實際裝置寬度解碼,不把完整 2560px 圖片長駐記憶體
  • ETA Provider 只通知選取車站與 ETA 區域

目前測試涵蓋 ETA 格式、10 秒更新及生命週期、重複請求保護、相同 ETA List identity、城巴跨資料集站序差異、九巴站序驗證、搜尋前綴、最近搜尋順序、2,500 個方向搜尋低於 50ms、背景圖片比例/放大/損壞處理,以及路線卡片必要欄位。

驗證

dart format --output=none --set-exit-if-changed lib test
flutter analyze
flutter test
flutter build apk --release

Android QA 應再以實際 API 執行以下流程:開啟背景設定及系統相簿、裁剪圖片、搜尋 970、開啟兩個方向、展開不同車站、確認三班 ETA、收藏後返回首頁,以及檢查 logcat 沒有 Flutter exception。

About

香港巴士實時到站時間 Flutter App,支援九巴、龍運及城巴官方 API

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages