Реализация трекинга тренировок в мобильном приложении
Трекинг тренировки — это управляемая сессия с несколькими параллельными потоками данных: 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 месяца.







