При розробці трекера тренувань ключова проблема — точність 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 місяці.
Зв'яжіться з нами для оцінки вашого проєкту — ми підберемо оптимальне рішення під ваш стек і бюджет. Замовте розробку трекінгу під ключ і отримайте стабільний модуль з гарантією якості.







