Skip to content

Refactor/home page - #518

Merged
whes1015 merged 93 commits into
mainfrom
refactor/home-page
Aug 6, 2026
Merged

Refactor/home page#518
whes1015 merged 93 commits into
mainfrom
refactor/home-page

Conversation

@whes1015

@whes1015 whes1015 commented Aug 2, 2026

Copy link
Copy Markdown
Member

No description provided.

vendored MapLibre 是 6.19.1,但 pubspec.yaml 早已寫 ^0.26.2(commit 9992448
只改 yaml、沒更新 lock 與 vendored 二進位),舊 lock 一直遮住這個矛盾;換新
Flutter SDK 重新解析後 pod install 直接衝突(0.26.2 硬性要求 MapLibre = 6.27.0)。

- 重新 vendor MapLibre 6.27.0:自官方 maplibre-native ios-v6.27.0 下載
  MapLibre.dynamic.xcframework.zip,SHA256 與 maplibre-gl-native-distribution
  的 checksum 逐字元吻合;framework 內版本戳記確認 6.27.0。podspec 同步更新。
- maplibre_gl ^0.25.0 → ^0.26.2。Dart API 無破壞性變更,analyze 全乾淨。
- 開啟 enable-swift-package-manager。Flutter 確認所有 iOS 外掛皆為 Swift
  Package;SPM 解析出 15 個套件(maplibre 6.27.0、firebase-ios-sdk 12.15.0、
  iosawncore 0.12.1 等),提交 Package.resolved 以固定版本。
- Podfile 移除 pod 'MapLibre':SPM 已提供並嵌入 MapLibre,兩者並存會讓
  Runner.app/Frameworks/MapLibre.framework 由兩個 build command 產出而建置失敗。
  vendored 那份保留為 CocoaPods fallback(關掉 SPM 時把該行加回即可)。

驗證:三個 gate、format、241 測試全過;iOS 模擬器建置成功並啟動,地圖(向量圖磚
+ 雷達回波 + 行政區界)渲染正常。pbxproj 與 Podfile.lock 依慣例未納入提交
(Flutter 會自行移除已無作用的 [CP] Embed/Copy Pods 階段)。
移除 Podfile 後建置仍失敗:
  Error (Xcode): Framework 'Pods_Runner' not found
  Linker command failed with exit code 1

CocoaPods 會產生 Pods_<Target>.framework 傘型產物並連進 target,SPM 下不存在
這個產物,連結就斷。清掉 Runner 與 RunnerTests 的 Pods_*.framework 連結、其
PBXFileReference,以及只裝這兩者、清空後的 Frameworks 群組(以 xcodeproj gem
程式化處理,非手改文字)。

至此 pbxproj 對 Pods_ / Pods- / [CP] / PODS_ROOT / Manifest.lock / CocoaPods
全拼法皆為 0 匹配,FlutterGeneratedPluginSwiftPackage 接線完好。

驗證:模擬器建置成功並啟動,地圖(向量圖磚 + 雷達回波)渲染正常、Firebase
initialized、零執行期錯誤;三個 gate、format、analyze、241 測試全過。
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔍 OpenCodeReview found 22 issue(s) in this PR.

  • ✅ Successfully posted inline: 22 comment(s)

CocoaPods 移除後 Runner.xcworkspace/contents.xcworkspacedata 仍留著
group:Pods/Pods.xcodeproj 的 FileRef。不會擋建置(Xcode 容忍缺檔的 FileRef),
但在 Xcode 專案導覽器會顯示為缺失群組,且與 Flutter 3.44 的 create 模板不符
(該模板只含 Runner.xcodeproj 一個 FileRef)。

驗證:flutter build ios --simulator --debug 成功(✓ Built Runner.app)。
@pull-request-size pull-request-size Bot added size/M and removed size/S labels Aug 2, 2026
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Aug 2, 2026
Comment on lines +341 to +345
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_appliedCode = null;
_applySelection();
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · high]
當目前的選擇是全台灣 (code 為 null) 時,將 _appliedCode 設為 null 會導致 code == _appliedCode 成立。如果 styleEpoch 未變,_applySelection 會直接 return,導致地圖尺寸改變時無法重新對焦。建議同時將 _appliedCodeEpoch 設為 -1。

Suggestion:

Suggested change
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_appliedCode = null;
_applySelection();
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_appliedCode = null;
_appliedCodeEpoch = -1;
_applySelection();
});

Comment on lines +139 to +142
// The map's own size, not MediaQuery's screen size: this surface sits inside
// the shell (below the status bar, above the nav bar), so the two differ by a
// device-dependent amount and a zoom derived from the screen mis-frames.
final size = _mapViewSize ?? MediaQuery.sizeOf(context);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · medium]
_mapViewSize 的更新與使用可能存在不一致性。_mapViewSize 是在 build 階段透過 LayoutBuilder 更新的,而 _frameBounds 可能在生命週期中的其他時機點被呼叫(例如 _onStyleLoaded)。若在佈局尚未完成或正在變動時觸發 _frameBounds,可能會導致使用到舊的(或 fallback 的 MediaQuery)視口尺寸,進而影響地圖瓦片加載的精確度。

全國框景取自 BaseMap.taiwanBounds,而該框的座標本身是錯的:
  southwest (22.2, 119)      西緣深入海峽約 110 km
  northeast (25.35, 121.05)  東緣 121.05 落在島內,宜蘭/花蓮/臺東全在框外

於是框的中心經度是 120.02(島的真實中心約 121.0),fit 之後臺灣被推到畫面右側、
東海岸被切掉,左半則是空海——與註解宣稱的「不被開闊海面主宰」正好相反。換不同
長寬比的手機只是把這個既有偏斜放得更明顯。

改為本島實際極點加少量邊界:富貴角 25.30N、鵝鑾鼻 21.90N、國聖港 120.04E、
三貂角 122.01E → (21.87, 119.99) ~ (25.32, 122.05),中心經度 121.02。

同時改成 getter:lazily-initialised 的 static final 在熱重載後會保留舊值,改這些
數字會看起來毫無作用。

新增 test/shared/map/base_map_test.dart 釘住四個極點都在框內、中心不偏離島嶼、
且不灌入過多海面,避免再度悄悄劣化。245 測試通過。
兩個問題:
1. 地圖是 Positioned.fill 全螢幕,RegionBar 疊在上方、拖盤疊在下方(見
   home_page.dart 的 Stack)。boundsFitCamera 只有 bottomInset、沒有 topInset,
   所以上緣被 RegionBar 遮住的部分從未扣除,框出來的內容偏上且被切。
   加入 topInset:可用高度扣掉上下兩端,相機中心改位移 (bottomInset - topInset)/2
   —— 拖盤把框往上抬、頂部列把框往下推。首頁傳入 status bar + RegionBar.height。
   RegionBar 的 44 改為公開常數,不再是散落的魔術數字。
2. 全國需含金門。舊框西緣 119.99 只到本島西岸,金門(烈嶼約 118.24E)在框外。
   西緣改為 118.15;澎湖落在臺灣—金門的跨距內,順帶涵蓋。馬祖(約 26.15N)仍在
   框外,需要的話再往北擴。

實測數值(iPhone 17 Pro,402x874):可視帶 354x432,fit zoom 6.00(寬為限制軸),
框內容渲染 354x342,完整落在可視帶內。

測試:base_map_test 改為驗證金門/烈嶼/澎湖與本島極點皆在框內、西緣不過度外擴;
camera_fit_test 新增 topInset 四項(往下推、上下相等則置中但縮小、兩端都縮減、
兩端相加超過高度回傳 null)。248 測試通過。
三個問題同根:地圖只在首次載入框一次(_framed),之後換圖層既不重算遮擋高度、
也不重新對焦目標。

- 新增 MapLayer.bottomChromeFraction:每個圖層宣告自己「常駐」chrome 佔地圖高度的
  比例。測站層回傳 StationSheet.peekExtent、颱風層回傳 TyphoonPanel.peekExtent、
  RTS 回傳 RtsMonitorPanel.bottomStripFraction;雷達回傳 0,因為時間軸是 scaffold
  自己擁有的元件,改以 GlobalKey 量測真實高度(內容會變高度,宣告不準)。
  三個面板的高度常數一併公開,消除散落的魔術數字。
- scaffold 保留 _target(框景目標)獨立於相機。_applyFraming() 以「目標 + 當前
  可視帶」重算,觸發點:樣式載入、handoff、換圖層、時間軸量到新高度。
  於是選了鄉鎮再切雷達回波,鄉鎮仍是主體;而各圖層遮擋高度不同也各自正確。
- 拖盤只回報「靜止」高度:使用者自己拖開拖盤不該移動相機。
- 上緣改為扣除 status bar。
- 移除 MapCameraHandoff.bottomInsetFraction:地圖現在自行推導遮擋,不需要首頁把
  比例傳過來(首頁與地圖的 chrome 本來就不同,強制同一相機反而是錯的)。
  handoff 現在只交接「地理範圍」。

功能不變,行為更正確。三個 gate、analyze、248 測試全過。
@pull-request-size pull-request-size Bot added size/XL and removed size/L labels Aug 2, 2026
setCurrentCode 全專案只有兩個呼叫點:app.dart 啟動時一次,以及 LocationMonitor
在「不可用→可用」轉換時。那條 250m 的前景定位串流只餵給伺服器上傳,沒有任何人
用它更新所在地。結果:在臺南開 app、移動到臺北,所在地仍是臺南,直到重開 app 或
把定位服務關掉再開。對防災 app 而言等於對著錯誤的行政區顯示與示警。
第一次定位若逾時(冷啟動、室內很常見)也沒有重試,整個 session 所在地都是空的。

- DeviceLocationReporter 新增 fixes 廣播串流,發布每個被接受的定位。刻意在上傳
  「之前」發布:token 還沒註冊或上傳失敗時,所在地仍須跟上。
  共用同一條串流而非各開一條——每次 getPositionStream 都會啟動獨立的原生定位
  session,兩個訂閱等於兩份 GPS 耗電算同一個座標。
- LocationMonitor 訂閱該串流,解析行政區並在「有變化時」發布,於是所在地持續跟隨
  使用者;串流本身也讓首次定位失敗能自行痊癒(不再需要重開 app)。
- LocationService.townAt 公開:已持有定位的呼叫端不必再抓一次。
- getLastKnownPosition 加上 10 分鐘時效上限。它原本無條件採用,可能是數小時前、
  其他縣市的定位——那會直接標錯所在地。逾時則走 getCurrentPosition。

測試:新增 location_monitor_test(跨行政區移動會更新、同行政區內移動不擾動監聽者、
dispose 後停止追蹤),reporter 新增兩項(fixes 廣播、上傳丟例外仍發布)。
三個 gate、analyze、253 測試全過。
Comment on lines +153 to +157
final size = _mapViewSize ?? MediaQuery.sizeOf(context);
// The map fills the page, but the region bar is layered over its top and the
// sheet over its bottom, so the band actually visible is shorter at both
// ends. Frame into that band, not the whole map.
final topInset = MediaQuery.paddingOf(context).top + RegionBar.height;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[other · low]
目前的 topInset 計算方式為 MediaQuery.paddingOf(context).top + RegionBar.height。這假設了 RegionBar 的高度是靜態且始終位於地圖頂部的。如果 RegionBar 的顯示狀態或高度會動態變化(例如透過動畫縮放),則 topInset 可能無法準確反映實際的視覺遮擋,進而導致地圖對焦偏移。建議確認 RegionBar.height 是否能反映實時的視覺高度。

Comment on lines +340 to +351
final measured = constraints.biggest;
if (measured != _mapViewSize && measured.isFinite) {
_mapViewSize = measured;
// Re-frame once the new size is laid out. Bypasses the
// unchanged-selection guard, which only tracks the code, not the size.
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_appliedCode = null;
_applySelection();
});
}
return BaseMap(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[maintainability · low]
LayoutBuilderbuilder 方法中直接修改 _mapViewSize 屬於副作用操作。雖然您使用了 WidgetsBinding.instance.addPostFrameCallback 來避開在 build 階段呼叫 setState 的錯誤,但直接在 builder 中修改狀態變數仍不是最佳實踐。建議考慮將此邏輯移至 didChangeDependencies 或透過其他方式監控佈局變化,以符合 Flutter 的單向數據流原則。

Comment on lines +26 to +33
expect(
point.latitude,
inInclusiveRange(
bounds.southwest.latitude,
bounds.northeast.latitude,
),
reason: '$name latitude falls outside taiwanBounds',
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug · medium]
測試程式碼中使用了 inInclusiveRange 這個 matcher,但在目前的 import 列表中並未發現其定義或來源。如果這不是專案中自定義的 extension,將導致編譯錯誤。建議改用 flutter_test 原生的 greaterThanOrEquallessThanOrEqual

Suggestion:

Suggested change
expect(
point.latitude,
inInclusiveRange(
bounds.southwest.latitude,
bounds.northeast.latitude,
),
reason: '$name latitude falls outside taiwanBounds',
);
expect(
point.latitude,
greaterThanOrEqual(bounds.southwest.latitude),
reason: '$name latitude falls below taiwanBounds',
);
expect(
point.latitude,
lessThanOrEqual(bounds.northeast.latitude),
reason: '$name latitude falls above taiwanBounds',
);

訊息其實早就存在(regionCurrentUnavailable),但被放在 home_content 的內容區、
落在拖盤靜止高度的視線之外——使用者看到的是標頭寫著「所在地」配一排讀數,讀起來像
「我們在所在地,天氣不明」,而不是「我們定位不到你」。

- 改在 HomeSheetHeader 呈現:所在地無定位時,以定位關閉圖示 + 訊息「取代」整排
  讀數。沒有位置就沒有該位置的天氣,列出一排破折號等於暗示我們知道你在哪。
- 移除 home_content 內重複的同一段文字(含已無用的 _CurrentUnavailableNotice),
  避免拖開拖盤後同一句話出現兩次。
- 修掉捏造值:降雨原本是 rain?.toStringAsFixed(1) ?? '0.0',無資料時顯示
  「0.0 mm」——與真實的「沒有降雨」完全無法區分。改為 '—',與濕度一致。
- 中文語系措辭改為「無法取得所在地位置資訊」(zh / zh_Hant_HK),簡體對應調整;
  其餘 7 個語系語意相同,維持原譯。

測試:新增三項(無定位顯示訊息與圖示、無定位不出現任何讀數、有定位則顯示讀數
而非訊息)。三個 gate、analyze、256 測試全過。
- data/event_api.dart:getHistoryList(全國)與 getHistoryRegion({region})。
  兩者不是同一份資料的前端過濾——事件是否涵蓋某鄉鎮由伺服器依 area/polygon 判定,
  前端無從計算,所以全國與鄉鎮是不同請求。走 ApiTier.legacyApi(api-1)。
- domain/event_repository.dart 回傳 Result<List<Event>>:空時間軸不能有歧義,
  「這裡沒發生事」與「連不上伺服器」在畫面上長一樣是防災 app 不能接受的。
- Event.fromJson 依「所檢視的鄉鎮」挑選文字。這裡有個真實陷阱:wire 的
  text.description 是以鄉鎮代碼為鍵的 map,而 "all" 並非泛用備援——實測同一則
  大雨特報裡 all 放的是「新北市三峽區」的句子,同時另有 112 個鄉鎮各自的句子。
  若拿 all 當備援,永康區使用者會讀到三峽區的災害描述。因此 all 只在它是 map
  唯一鍵時才採用。
- 映射全面防禦性:欄位皆可選,id 空則用 key、再退回送出時間;無時間戳的項目直接
  捨棄(無法放上時間軸);未知 type 仍以一般警報呈現而非消失;單筆壞資料只跳過
  該筆,不讓整份危害清單變空白。
- 時間軸改由 AsyncView 驅動(loading/empty/error+重試),頁面依各分頁的區域取碼;
  所在地無定位時退回全國,而非顯示空清單。移除假資料與其 l10n-ignore-file 豁免。

驗證:對線上 API 實測,全國 5 筆、永康區 1 筆且描述正確為「臺南市永康區」;
新增 10 項映射測試(含 all 陷阱三種情境)。三個 gate、analyze、266 測試全過。
@whes1015
whes1015 merged commit bc5c911 into main Aug 6, 2026
@whes1015
whes1015 deleted the refactor/home-page branch August 6, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants