Реалізація трекінгу тренувань у мобільному приложенні
Трекінг тренування — це керована сесія з кількома паралельними потоками даних: GPS, акселерометр/гіроскоп, ЧСС (з носимого пристрою), барометр (висота). Все це потрібно записувати синхронно, відображати в реальному часі та зберігати без втрат при падінні приложення або розряді батареї.
Архітектура сесії тренування
Центральний елемент — WorkoutSession (або як вы її називаєте), кінцевий автомат із станами:
Idle → Preparing → Active → Paused → Active → Finishing → Saved
Переходи тригерятися користувачем (кнопки Start/Pause/Finish) та системою (втрата GPS, розряд батареї). Весь стан зберігається в WorkoutRepository, персистуватися через Room (Android) або CoreData/SQLite (iOS) після кожного оновлення — щоб при краху відновити сесію.
@Entity
data class WorkoutPoint(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val sessionId: String,
val timestamp: Long,
val latitude: Double?,
val longitude: Double?,
val altitude: Double?,
val heartRate: Int?,
val speed: Double?,
val distance: Double
)
Кожні 5 секунд вставляємо нову запис в БД. При завершенні тренування — агрегуємо все в WorkoutSummary. Промежуточные точки не видаляємо — вони потрібні для побудови трека.
GPS-трекінг та точність
Фільтрація GPS-шуму
Сирі GPS-дані містять шум ±5–15 м. На маршруті це візуально виглядає як «зигзаги» замість прямого відрізка. Фільтруємо через Kalman-фільтр — найадекватніший метод для GPS.
Для мобільної розробки не потребується реалізовувати Kalman з нуля. На Android — FusedLocationProviderClient вже застосовує внутрішню фільтрацію. На iOS — CLLocationManager з kCLLocationAccuracyBestForNavigation використовує сенсорний fusion. Додатково: відбрасуємо точки з horizontalAccuracy > 20 м.
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]) {
guard let location = locations.last,
location.horizontalAccuracy <= 20,
location.horizontalAccuracy >= 0 else { return }
if let previous = lastLocation {
let segment = location.distance(from: previous)
totalDistance += segment
}
lastLocation = location
trackPoints.append(location)
}
Розрахунок дистанції та темпу
Дистанція — сумма дистанцій між послідовними GPS-точками (CLLocation.distance(from:) на iOS, Location.distanceTo() на Android). Темп (мин/км) = 1000 / швидкість (м/с) / 60. Швидкість беремо з CLLocation.speed / Location.speed — вони обраховуються по доплеровському зсуву, точніше ніж різниця координат.
При speed < 0 (немає достовірних даних) — використовуємо швидкість з координат.
Фоновое виконання
iOS
Додаємо в Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
Запитуємо дозвіл Always. Без цього GPS припиняється через ~15 секунд після переходу приложення в фон. allowsBackgroundLocationUpdates = true на CLLocationManager — обов'язково.
Android
Foreground Service з сповіщенням (інакше Android убьє процес при нехватці пам'яті):
class WorkoutTrackingService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildTrackingNotification()
startForeground(NOTIFICATION_ID, notification)
startLocationUpdates()
return START_STICKY
}
}
START_STICKY — система перезапустить сервіс якщо убьє процес, з null интентом. Обробляємо null та відновлюємо стан з Room.
Інтеграція з носимими пристроями
ЧСС від Apple Watch — через HKWorkoutBuilder на iOS (Watch автоматично додає семпли). На Android — Wear OS через HealthServicesClient або Bluetooth GATT з профілем Heart Rate (UUID 0x180D).
Bluetooth GATT для зовнішніх датчиків (нагрудний пояс Polar H10, Wahoo TICKR):
val hrServiceUUID = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb")
val hrCharacteristicUUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb")
override fun onCharacteristicChanged(gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic) {
if (characteristic.uuid == hrCharacteristicUUID) {
val flag = characteristic.properties
val format = if (flag and 0x01 != 0) {
BluetoothGattCharacteristic.FORMAT_UINT16
} else {
BluetoothGattCharacteristic.FORMAT_UINT8
}
val heartRate = characteristic.getIntValue(format, 1) ?: 0
onHeartRateReceived(heartRate)
}
}
Збереження в HealthKit / Health Connect
По завершенню тренування записуємо повний HKWorkout (iOS) або ExerciseSessionRecord (Android) зі всіма вкладеними метриками: дистанція, ЧСС, маршрут. На iOS — HKWorkoutRouteBuilder для GPS-трека. Користувач повинен бачити тренування в системному приложенні «Здоров'я» або Health Connect.
Терміни
Базовий трекер бігу з GPS, дистанцією та темпом — 3–5 тижнів. Повний трекер з ЧСС, BLE-датчиками, кількома типами активностей, експортом GPX та інтеграцією в платформені сховища — 2–4 місяці.







