При розробці трекера тренувань ключова проблема — точність GPS і безперервність запису у фоновому режимі. Навіть на флагманах сирий GPS дає шум 5–15 метрів, а система може вбити процес додатка через 15 секунд після згортання. Без правильної фільтрації трек рясніє «зигзагами», а втрата даних при краші — втрата мотивації користувача. Додатково, на Android без Foreground Service додаток може бути вбито будь-якої миті, а на iOS навіть з дозволом «Always» система може призупиняти оновлення при низькому заряді батареї. Особливо складний випадок, коли спортсмен біжить у лісосмузі — GPS-сигнал слабшає і трек починає «плавати» без Kalman-фільтра.
Ми пропонуємо модуль трекінгу під ключ, який вирішує ці задачі: від збору даних до інтеграції з платформенними сховищами здоров'я. Наша команда з 5-річним досвідом у мобільній розробці гарантує стабільність запису на iOS і Android. За 3–5 тижнів ви отримуєте готовий трекер бігу з GPS, дистанцією та темпом. Для складніших завдань — з ЧСС, BLE-датчиками та кількома типами активностей — термін складає 2–4 місяці. Ми використовуємо перевірені алгоритми та конфігурації, які показали високу точність у більш ніж 15 комерційних проєктах.
Тепер детально розберемо, як ми забезпечуємо точність треку, роботу у фоні та інтеграцію з носимими пристроями.
Архітектура сесії тренування
Центральний елемент — 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-фільтр — він точніший, ніж ковзне середнє, і адаптується до динаміки руху. На прямих ділянках Kalman дає точність ±2 м проти ±5 м у ковзного середнього; на поворотах — ±4 м проти ±10 м. У 80% проєктів ми використовуємо саме Kalman-фільтр — він дає приріст точності на 60% на поворотах.
Для мобільної розробки не потрібно реалізовувати Kalman з нуля. На Android — FusedLocationProviderClient уже застосовує внутрішню фільтрацію. На iOS — CLLocationManager з kCLLocationAccuracyBestForNavigation використовує сенсорний fusion. Додатково: відкидаємо точки з horizontalAccuracy > 20 м.
Кроки налаштування фільтрації:
- Ініціалізуйте LocationManager з точністю
kCLLocationAccuracyBestForNavigation(iOS) або створіть FusedLocationProviderClient (Android). - Встановіть мінімальну відстань оновлення — 5 м.
- У callback відкидайте точки з
horizontalAccuracy > 20. - Застосуйте Kalman-фільтр для згладжування решти точок.
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 (немає достовірних даних) — використовуємо швидкість з координат.
Чому важливий фоновий режим?
Без фонової роботи GPS вимикається через 15 секунд після згортання додатка. На iOS додаємо UIBackgroundModes = location в Info.plist і запитуємо дозвіл «Always». На Android запускаємо Foreground Service з повідомленням — інакше система вб'є процес при нестачі пам'яті.
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.
Чек-лист налаштування фонового режиму для iOS і Android
iOS:
- Додати UIBackgroundModes = location в Info.plist
- Запросити дозвіл на завжди (requestAlwaysAuthorization)
- Використовувати allowsBackgroundLocationUpdates = true
- Для ініціації сесії використовувати CLMonitor (починаючи з iOS 17)
Android:
- Оголосити FOREGROUND_SERVICE_LOCATION дозвіл (з API 29)
- Запустити startForeground з каналом повідомлення
- Використовувати START_STICKY для перезапуску
- Обробляти null Intent в onStartCommand
Порівняння методів фільтрації GPS
| Метод | Точність на прямій | Точність на поворотах | Обчислювальна складність |
|---|---|---|---|
| Ковзне середнє | ±5 м | ±10 м | Низька |
| Kalman-фільтр | ±2 м | ±4 м | Середня |
| Системний FusedLocation | ±3 м | ±5 м | Низька (вбудований) |
Порівняння фонової роботи iOS vs Android
| Платформа | Механізм | Вимоги | Надійність |
|---|---|---|---|
| iOS | Background Location | Permission «Always», UIBackgroundModes | Висока, але може призупинити при низькому заряді |
| Android | Foreground Service | Notification, START_STICKY | Дуже висока, перезапуск при вбивстві |
Інтеграція з носимими пристроями
ЧСС від 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. Health Connect API доступний на Android 10+.
Що входить в роботу
- Архітектурна документація та діаграми станів сесії
- Вихідний код з коментарями та unit-тестами
- Інтеграція з обраними датчиками (BLE, носимі)
- Налаштування фонового режиму та повідомлень
- Інструкція з деплою в App Store / Google Play
- Підтримка протягом 2 місяців після здачі
Терміни
Базовий трекер бігу з GPS, дистанцією та темпом — 3–5 тижнів. Повний трекер з ЧСС, BLE-датчиками, кількома типами активностей, експортом GPX та інтеграцією в платформові сховища — 2–4 місяці.
Зв'яжіться з нами для оцінки вашого проєкту — ми підберемо оптимальне рішення під ваш стек і бюджет. Замовте розробку трекінгу під ключ і отримайте стабільний модуль з гарантією якості.







