- バックグラウンドで出発済み列車が消えないバグを修正 (LiveActivityForegroundService: scheduledMinutes+delay<nowMinutes で除外) - parseTimeToMinutes() ヘルパーを Kotlin に追加 - UI側も遅延加算した発車時刻で出発済み判定するよう統一 (StationDiagramView: getDelayMinutes + dayjs.add) - between.ts の BetweenStation を使った接近セクション判定を実装 (棒線駅で列車がセクション手前に居る場合は未到達扱いで保持) - StationTrainInfo に sectionStation フィールドを追加 (prevStop/nextStop を廃止し API Pos と直接照合可能な文字列に変更) - docs/station-lock-dual-impl.md 作成(KotlinとTS間のデュアル実装ガイド)
7.5 KiB
駅固定(駅ロック)通知バナー — デュアル実装ガイド
Kotlin(Android バックグラウンドサービス)と TypeScript(React Native UI)の
2か所に同一ロジックが存在する。言語が異なるため共通化は不可能。
一方を変更したら必ず他方も追従すること。
実装ファイルマップ
| 役割 | ファイル | 主要箇所 |
|---|---|---|
| Android バックグラウンドポーリング・通知更新 | modules/expo-live-activity/android/.../LiveActivityForegroundService.kt |
pollStationTrains() |
| 表示行フォーマット構築(Android) | 同上 | buildDisplayLines() |
| 時刻→分変換(Android) | 同上 | parseTimeToMinutes() |
| JS↔Native ブリッジ・表示行フォーマット構築(TS) | modules/expo-live-activity/src/index.ts |
buildStationLockLines() |
| React フック(状態管理) | lib/useStationLockActivity.ts |
useStationLockActivity() |
| UI + nextTrain 探索 + LiveActivity 起動 | components/StationDiagram/StationDiagramView.tsx |
useEffect(L218付近) |
重複ロジック一覧(両側を同期すること)
1. 表示行フォーマット — buildDisplayLines ↔ buildStationLockLines
フォーマット: 時刻 種別 [列車名] 行き先行 遅延状況(全角スペース区切り)
// LiveActivityForegroundService.kt — buildDisplayLines()
parts.add(t.optString("time", ""))
parts.add(t.optString("typeName", ""))
if (name.isNotEmpty()) parts.add(name) // trainName
parts.add("${t.optString("destination", "")}行")
parts.add(t.optString("delayStatus", "定刻"))
lines.add(parts.joinToString(" ")) // 全角スペース
// index.ts — buildStationLockLines()
const parts = [t.time, t.typeName];
if (t.trainName) parts.push(t.trainName);
parts.push(`${t.destination}行`);
parts.push(t.delayStatus || '定刻');
return parts.join(' '); // 全角スペース
変更時: 両ファイル同時に修正。区切り文字・フィールド順・
行サフィックスをそろえること。
2. 出発済み列車の除外判定
| Kotlin(バックグラウンド) | TypeScript(UI) | |
|---|---|---|
| 現在時刻取得 | Calendar.getInstance() → 分単位 |
dayjs() |
| 時刻フォーマット | parseTimeToMinutes("HH:mm") |
dayjs(d.time, "HH:mm") |
| 遅延加算 | あり scheduledMinutes + delay >= nowMinutes |
あり .add(delay, "minute").isAfter(now) |
| パース失敗時 | -1 を返し表示継続(安全側) |
trainPosition が不完全な場合は時刻判定にフォールバック |
| 接近セクション判定 | prevStop + Pos.startsWith("$prevStop~") |
isInApproachSection() (allTrainDiagram + trainPosition) |
API の位置報告はセクション単位(例: "橋岡~鴨川")のため、そのセクション内の棒線駅では
遅延が未更新でも列車が残っている可能性がある。セクション内にいることを根拠に表示し続ける。
// LiveActivityForegroundService.kt — pollStationTrains()
val prevStop = t.optString("prevStop", "") // buildTrainsInfo() で JS 側が付与
val matchedPos = matchedTrain?.optString("Pos", "") ?: ""
val isInApproachSection = prevStop.isNotEmpty() &&
matchedPos.isNotEmpty() &&
matchedPos.startsWith("$prevStop~")
val scheduledMinutes = parseTimeToMinutes(t.optString("time", ""))
if (scheduledMinutes < 0 || scheduledMinutes + delay >= nowMinutes || isInApproachSection) {
filtered.put(t) // 接近中なら時刻を問わず残す
}
// StationDiagramView.tsx — isInApproachSection()
const pos = trainPosition(currentTrain.find(t => t.num === trainNumber));
if (!pos.isBetween) return false;
// allTrainDiagram のルート順で from < 当駅 <= to なら接近セクション内
const stops = allTrainDiagram[trainNumber].split("#").map(e => e.split(",")[0]);
const lo = Math.min(stops.indexOf(pos.Pos.from), stops.indexOf(pos.Pos.to));
const hi = Math.max(stops.indexOf(pos.Pos.from), stops.indexOf(pos.Pos.to));
return stIdx > lo && stIdx <= hi;
// trainTimeFiltering.ts の default: ブロックにも同等ロジック追加済み
prevStop は FixedStationBox.tsx の buildTrainsInfo() 内で allTrainDiagram から計算し
trainsJson 各エントリに付与。Android はこの値で Pos 文字列の from 部分を照合する。
上り・下り両対応: allTrainDiagram は走行方向順で格納されているため prevStop も方向自動対応。
3. delayStatus テキスト生成
// LiveActivityForegroundService.kt
"${delay}分遅れ" // delay > 0
"定刻" // delay == 0
// index.ts — buildStationLockLines()
t.delayStatus || '定刻' // Kotlin 側が生成した値を受け取るだけ
Kotlin 側が正規化して書き込み、TS 側は受け取るだけ。
テキストフォーマットを変える場合は Kotlin 側のみ修正すればよい。
データ構造(共通フィールド名)
StationTrainInfo(trainsJson の各要素)
| フィールド | 型 | 説明 | キー名(Kotlin) |
|---|---|---|---|
time |
"HH:mm" |
発車時刻 | "time" |
typeName |
string |
種別名 | "typeName" |
trainName |
string |
列車名(なければ "") |
"trainName" |
destination |
string |
行き先 | "destination" |
platform |
string |
番線 | "platform" |
delayStatus |
string |
"定刻" or "N分遅れ" |
"delayStatus" |
trainNumber |
string |
列車番号(照合用) | "trainNumber" |
フィールド名はスネークケースではなくキャメルケース。変更時は Kotlin の optString("xxx") とも一致させること。
データフロー
[StationDiagramView.tsx]
↓ startStationLock({ trains: StationTrainInfo[], ... })
[index.ts / ExpoLiveActivityModule]
↓ ACTION_START_STATION intent (trainsJson, stationTitle, stationColor, ...)
[LiveActivityForegroundService.kt]
→ stationTitle, stationColor, trainsJson を保持
→ 定期ポーリング: pollStationTrains()
① API で遅延取得 → delayStatus を上書き
② 出発済み列車を除外 → trainsJson を更新
③ buildDisplayLines() → 通知を更新
[StationDiagramView.tsx] (フォアグラウンド時)
→ stationLock.update({ nextTrainTime, ... }) ※ trains は渡さない
→ Android 側の trainsJson は更新されない(ポーリングで自律管理)
注意: フォアグラウンド時の
stationLock.update()はnextTrainTime/nextTrainDestination等の
StationLockStateのみを更新し、trains(詳細リスト)は更新しない。
詳細リストはバックグラウンドポーリングが独立して管理している。
チェックリスト(ロジック修正時)
- 表示行フォーマットを変えた →
buildDisplayLines(Kotlin) とbuildStationLockLines(TS) 両方修正 - 除外閾値を変えた(例: 1分以上余裕を持たせる)→ Kotlin
pollStationTrainsと TSStationDiagramView両方修正 - 新フィールドを追加した →
StationTrainInfo(TS型定義) と Kotlin のoptStringキー名を一致させる "定刻"/"N分遅れ"のテキストを変えた → Kotlin 側のみでよい(TS は受け取るだけ)