Как разрабатывать функции для тренировок с помощью платформы "Здоровье и спорт"

Это руководство совместимо с платформой "Здоровье и спорт" версии 1.1.0 и более поздних.

Если вы хотите создать в своем приложении функцию тренировок, то можете использовать платформу "Здоровье и спорт", чтобы:

  • Записывать данные о тренировках
  • Записывать маршруты тренировок
  • Запись показателей тренировки, таких как пульс, скорость и расстояние
  • Чтение данных о тренировках из других приложений

В этом руководстве описывается, как создавать эти функции тренировок, включая типы данных, фоновое выполнение, разрешения, рекомендуемые рабочие процессы и рекомендации.

Обзор: создание комплексного трекера тренировок

Чтобы создать комплексное решение для отслеживания тренировок с помощью платформы "Здоровье и спорт", выполните следующие основные действия:

  • Правильно запрашивать разрешения на доступ к данным о здоровье.
  • Запись сеансов с помощью ExerciseSessionRecord.
  • Записывать данные о тренировке на протяжении всего занятия.
  • Правильно настройте фоновое выполнение, чтобы обеспечить непрерывный сбор данных.
  • Чтение данных о сеансах для сводок и анализа после тренировки.

Этот рабочий процесс обеспечивает совместимость с другими приложениями платформы "Здоровье и спорт" и проверяет доступ к данным, контролируемый пользователем.

Подготовка

Перед реализацией функций тренировок:

Основные понятия

Платформа "Здоровье и спорт" представляет данные о тренировках с помощью нескольких основных компонентов. ExerciseSessionRecord – это основная запись о тренировке, содержащая такие сведения, как время начала и окончания, а также тип упражнения. Во время сеанса могут записываться различные типы данных, например HeartRateRecord или SpeedRecord. Для занятий на открытом воздухе ExerciseRoute сохраняет данные GPS, которые связаны с соответствующим занятием.

Тренировки

ExerciseSessionRecord – это центральная запись для данных о тренировках, представляющая собой одно занятие. В каждой записи хранится:

  • startTime
  • endTime
  • exerciseType
  • Необязательные метаданные сеанса (название, заметки)

ExerciseSessionRecord также может содержать маршруты тренировок, круги и сегменты. Кроме того, во время тренировки могут записываться и другие типы данных, например HeartRateRecord или SpeedRecord.

Связанные типы данных

Данные, связанные с тренировками, представлены отдельными типами записей. Вот некоторые из них:

  • HeartRateRecord – серия измерений пульса.
  • SpeedRecord – представляет собой серию измерений скорости.
  • DistanceRecord – расстояние, пройденное между измерениями.
  • TotalCaloriesBurnedRecord – общее количество сожженных калорий между измерениями.
  • ElevationGainedRecord – набор высоты между измерениями.
  • StepsCadenceRecord – частота шагов между измерениями.
  • PowerRecord – выходная мощность между измерениями, часто используется при таких видах активности, как езда на велосипеде.

Полный список типов данных можно найти в статье Типы данных платформы "Здоровье и спорт".

Маршруты тренировок

Вы можете связать маршрут с тренировками на улице, используя ExerciseRoute. Маршруты состоят из последовательных объектов ExerciseRoute.Location, каждый из которых содержит:

  • Широта и долгота
  • Высота (необязательно)
  • Необязательный азимут
  • Информация о точности
  • Временная метка

Маршруты сеансов связи

Объект ExerciseRoute содержит последовательные данные о местоположении для тренировки. Она не считается отдельной записью в приложении "Здоровье и спорт". Вместо этого вы указываете данные ExerciseRoute при добавлении или обновлении ExerciseSessionRecord.

Рекомендации по разработке

Приложения для отслеживания тренировок часто должны работать в течение длительного времени, в том числе в фоновом режиме, когда экран выключен. При создании функций для тренировок важно учитывать, как управлять фоновым выполнением и запрашивать необходимые разрешения для доступа к данным о тренировках.

Выполнение в фоновом режиме

Приложения для тренировок обычно работают с выключенным экраном. В этом случае следует использовать:

  • Активные службы для определения местоположения и сбора данных с датчиков
  • WorkManager для отложенной записи или синхронизации;
  • Стратегии пакетной обработки для регулярной записи данных

Чтобы обеспечить непрерывность, используйте один и тот же идентификатор сеанса для всех записей.

Разрешения

Прежде чем читать или записывать данные о тренировках, приложение должно запросить соответствующие разрешения Здоровья и спорта. Обычно для тренировок запрашиваются разрешения на доступ к данным о занятиях, маршрутах и показателях, таких как пульс или скорость. Примеры:

  • Тренировки. Разрешения на чтение и запись для типа данных ExerciseSessionRecord.
  • Маршруты тренировок. Разрешения на чтение и запись для ExerciseRoute.
  • Пульс. Разрешения на чтение и запись для HeartRateRecord.
  • Скорость. Разрешения на чтение и запись для SpeedRecord.
  • Расстояние. Разрешения на чтение и запись для DistanceRecord.
  • Калории. Разрешения на чтение и запись для TotalCaloriesBurnedRecord.
  • Набор высоты. Разрешения на чтение и запись для ElevationGainedRecord.
  • Частота шагов. Разрешения на чтение и запись для StepsCadenceRecord.
  • Power. Разрешения на чтение и запись для PowerRecord.
  • Шаги. Разрешения на чтение и запись для StepsRecord.

Ниже показано, как запросить несколько разрешений для тренировки, которая включает данные о маршруте, пульсе, расстоянии, калориях, скорости и шагах.

После создания экземпляра клиента приложение должно запросить разрешения у пользователя. У пользователей должна быть возможность в любой момент предоставить или отозвать разрешения. Для этого создайте набор разрешений для нужных типов данных. Убедитесь, что разрешения в наборе сначала объявлены в манифесте Android.

val permissions =
    setOf(
        HealthPermission.getReadPermission(ExerciseSessionRecord::class),
        HealthPermission.getWritePermission(ExerciseSessionRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class),
        HealthPermission.getReadPermission(SpeedRecord::class),
        HealthPermission.getWritePermission(SpeedRecord::class),
        HealthPermission.getReadPermission(DistanceRecord::class),
        HealthPermission.getWritePermission(DistanceRecord::class),
        HealthPermission.getReadPermission(TotalCaloriesBurnedRecord::class),
        HealthPermission.getWritePermission(TotalCaloriesBurnedRecord::class),
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class)
    )
Используйте getGrantedPermissions, чтобы проверить, есть ли у приложения необходимые разрешения. Если нет, запросите их с помощью createRequestPermissionResultContract. Откроется экран разрешений платформы "Здоровье и спорт".
val permissions = setOf(
        HealthPermission.getReadPermission(StepsRecord::class),
        HealthPermission.getWritePermission(StepsRecord::class),
        HealthPermission.getReadPermission(HeartRateRecord::class),
        HealthPermission.getWritePermission(HeartRateRecord::class)
    )

val requestPermissionsLauncher = rememberLauncherForActivityResult(
    contract = PermissionController.createRequestPermissionResultContract()
) { grantedPermissions ->
    if (grantedPermissions.containsAll(permissions)) {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions granted!") }
    } else {
        coroutineScope.launch { snackbarHostState.showSnackbar("Permissions denied.") }
    }
}
Поскольку пользователи могут предоставлять или отзывать разрешения в любое время, ваше приложение должно проверять разрешения каждый раз перед их использованием и обрабатывать сценарии, в которых разрешение отсутствует.

Чтобы запросить разрешения, вызовите функцию checkPermissionsAndRun:

if (!granted.containsAll(permissions)) {
    // Check if required permissions are not granted, and return
    return emptySet()
}
// Permissions already granted; proceed with inserting or reading data

Если вам нужно запросить разрешения только для одного типа данных, например пульса, включите в набор разрешений только этот тип данных:

Доступ к данным о пульсе защищен следующими разрешениями:

  • android.permission.health.READ_HEART_RATE
  • android.permission.health.WRITE_HEART_RATE

Чтобы добавить в приложение возможность отслеживать пульс, запросите разрешения для типа данных HeartRateRecord.

Вот разрешение, которое нужно объявить, чтобы записывать данные о пульсе:

<application>
  <uses-permission
android:name="android.permission.health.WRITE_HEART_RATE" />
...
</application>

Чтобы считывать данные о пульсе, запросите следующие разрешения:

<application>
  <uses-permission
android:name="android.permission.health.READ_HEART_RATE" />
...
</application>

Провести тренировку

В этом разделе описывается рекомендуемый рабочий процесс для записи данных о тренировках.

Начните сеанс

Чтобы создать тренировку:

  1. Сгенерируйте уникальный идентификатор сеанса и убедитесь, что он стабилен. Если процесс приложения будет завершен и перезапущен, вы должны иметь возможность возобновить его с тем же идентификатором, чтобы предотвратить фрагментацию сеансов.
  2. Чтобы избежать дублирования при повторных попытках синхронизации, задайте metadata.clientRecordId.
  3. Напиши ExerciseSessionRecord. Укажи время начала.
  4. Начните собирать данные о типе данных и GPS. Это нужно делать только после успешной инициализации записи сеанса.

Пример:

val sessionClientId = UUID.randomUUID().toString()
val zoneOffset = ZoneOffset.systemDefault().rules.getOffset(startTime)

val session =   ExerciseSessionRecord(
    startTime = startTime,
    startZoneOffset = zoneOffset,
    endTime = startTime.plusSeconds(3600),
    endZoneOffset = zoneOffset,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    metadata = Metadata.activelyRecorded(clientRecordId = sessionClientId, device = Device(type = Device.TYPE_PHONE)),
)

healthConnectClient.insertRecords(listOf(session))

Запись маршрутов тренировок

Подробнее о том, как читать первичные данные…

При записи маршрута тренировки данные следует объединять в пакеты. Это означает, что вместо сохранения каждой отдельной точки GPS по мере ее появления вы собираете группу точек и сохраняете их все сразу за один вызов.

Это важно, потому что каждый раз, когда ваше приложение считывает или записывает данные на платформе "Здоровье и спорт", оно использует немного заряда батареи и вычислительной мощности.

В следующем примере кода показано, как записывать данные пакетами:

// 1. Create a list to hold your route locations
val routeLocations = mutableListOf<ExerciseRoute.Location>()

// 2. Add points to your list as the exercise happens
routeLocations.add(
    ExerciseRoute.Location(
        time = Instant.now(),
        latitude = 37.7749,
        longitude = -122.4194
    )
)

// ... keep adding points over a period of time ...

// 3. Save the whole list at once (Batching)
val session = ExerciseSessionRecord(
    startTime = startTime,
    endTime = endTime,
    exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
    // We pass the whole list here
    exerciseRoute = ExerciseRoute(routeLocations)
)

healthConnectClient.insertRecords(listOf(session))

Как завершить сеанс

После остановки сбора данных:

  • Обновление записи. Приложение обновляет ExerciseSessionRecord с помощью endTime.
  • Завершение работы с данными. При необходимости вы можете рассчитать сводные значения (например, общее расстояние или средний темп) и записать их в виде дополнительных записей.
val finishedSession = session.copy(endTime = Instant.now())
healthConnectClient.updateRecords(listOf(finishedSession))

Чтение данных о тренировке…

Приложения могут считывать данные о тренировках и связанную с ними информацию, чтобы обобщать сведения об активности, предоставлять рекомендации по здоровью или синхронизировать данные с внешним сервером. Например, вы можете прочитать ExerciseSessionRecord, а затем запросить HeartRateRecord или DistanceRecord, которые произошли в тот же интервал времени.

Если вам нужно синхронизировать данные о тренировках с бэкенд-сервером или поддерживать актуальность хранилища данных приложения с помощью Здоровья и спорта, используйте ChangeLogs. Это позволяет получить список вставленных, обновленных или удаленных записей с определенного момента времени, что более эффективно, чем отслеживание изменений вручную или повторное чтение всех данных. Подробнее о том, как синхронизировать данные с платформой "Здоровье и спорт"…

Чтение данных о сеансах

Чтобы прочитать данные о тренировках, используйте ReadRecordsRequest с типом ExerciseSessionRecord. Обычно вы фильтруете это по определенному диапазону времени.

suspend fun readExerciseSessions(
    healthConnectClient: HealthConnectClient,
    startTime: Instant,
    endTime: Instant
) {
    val response = healthConnectClient.readRecords(
        ReadRecordsRequest(
            recordType = ExerciseSessionRecord::class,
            timeRangeFilter = TimeRangeFilter.between(startTime, endTime)
        )
    )

    for (exerciseRecord in response.records) {
        // Process each session
        val exerciseType = exerciseRecord.exerciseType
        val notes = exerciseRecord.notes
    }
}

Чтение маршрутов

Хотя данные ExerciseRoute записываются в рамках тренировки, их нужно считывать отдельно. Используйте метод getExerciseRoute() с идентификатором сеанса, чтобы прочитать данные о маршруте:

suspend fun readExerciseRoute(
    healthConnectClient: HealthConnectClient,
    exerciseSessionRecord: ExerciseSessionRecord
) {
    // Check if the session has a route
    val route = healthConnectClient.getExerciseRoute(
        exerciseSessionRecordId = exerciseSessionRecord.metadata.id
    )

    when (route) {
        is ExerciseRouteResponse.Success -> {
            val locations = route.exerciseRoute.locations
            for (location in locations) {
                // Use latitude, longitude, and altitude
            }
        }
        is ExerciseRouteResponse.NoData -> {
            // Handle case where no route exists
        }
        is ExerciseRouteResponse.ConsentRequired -> {
            // Handle case where permissions are missing
        }
    }
}

Типы данных для чтения

Чтобы прочитать подробные данные (например, о пульсе) за определенное занятие, используйте startTime и endTime занятия для фильтрации запроса по этому типу данных.

suspend fun readHeartRateData(
    healthConnectClient: HealthConnectClient,
    exerciseSession: ExerciseSessionRecord
) {
    val response = healthConnectClient.readRecords(
        ReadRecordsRequest(
            recordType = HeartRateRecord::class,
            timeRangeFilter = TimeRangeFilter.between(
                exerciseSession.startTime,
                exerciseSession.endTime
            )
        )
    )

    for (heartRateRecord in response.records) {
        for (sample in heartRateRecord.samples) {
            val bpm = sample.beatsPerMinute
        }
    }
}

Рекомендации

Чтобы повысить надежность данных и удобство для пользователей, следуйте этим рекомендациям:

  • Часто записывайте данные во время активного отслеживания. При активном отслеживании записывайте данные по мере их поступления или с максимальным интервалом в 15 минут.
  • Используйте WorkManager для фоновой синхронизации. Используйте WorkManager для отложенной записи. Рекомендуем установить интервал в 15 минут, чтобы соблюсти баланс между актуальностью данных в реальном времени и экономией заряда батареи.
  • Пакетные запросы на запись. Не записывайте каждое событие датчика по отдельности. Разбивайте запросы на части. Платформа "Здоровье и спорт" обрабатывает до 1000 записей на запрос на запись.
  • Используйте стабильные и уникальные идентификаторы сеансов. Если сеанс редактируется или обновляется, использование того же идентификатора предотвращает его обработку как нового отдельного сеанса.
  • Используйте пакетную обработку для типов данных и точек маршрута. Чтобы снизить нагрузку на ввод-вывод и продлить время работы от батареи, группируйте элементы данных в один вызов insertRecords, а не записывайте каждый элемент отдельно.
  • Используйте идентификаторы клиентов, чтобы избежать дублирования данных. При создании записей задайте metadata.clientRecordId. Используется платформой "Здоровье и спорт" для идентификации уникальных записей. Если вы попытаетесь записать данные с уже существующим идентификатором clientRecordId, сервис "Здоровье и спорт" проигнорирует дубликат или обновит существующую запись, а не создаст новую. Задание metadata.clientRecordId – самый эффективный способ предотвратить дублирование при повторных попытках синхронизации или переустановке приложения.
    val record = StepsRecord(
        count = 100,
        startTime = startTime,
        endTime = endTime,
        startZoneOffset = ZoneOffset.UTC,
        endZoneOffset = ZoneOffset.UTC,
        metadata = Metadata(
            // Use a unique ID from your own database
            clientRecordId = "daily_steps_2023_10_27_user_123"
        )
    )
  • Проверьте существующие данные. Перед синхронизацией запросите диапазон дат, чтобы узнать, есть ли уже записи из вашего приложения.
  • Проверяйте точность GPS. Отфильтровывайте образцы GPS с низкой точностью (например, точки с большим радиусом горизонтальной точности), прежде чем записывать их в ExerciseRoute. Это поможет сделать карту понятной и профессиональной.
  • Убедитесь, что временные метки не пересекаются. Проверьте, что новый сеанс не начинается до окончания предыдущего. Перекрывающиеся сеансы могут привести к конфликтам в панелях фитнеса и сводных расчетах.
  • Четко объясняйте, зачем вам нужны разрешения. Используйте процесс Permission.createIntent, чтобы объяснить, зачем приложению нужен доступ к данным о здоровье. Например, можно написать: "Чтобы отслеживать динамику артериального давления и предоставлять статистику".
  • Поддержка приостановки и возобновления. Убедитесь, что приложение правильно обрабатывает приостановку. Когда пользователь ставит тренировку на паузу, прекратите сбор точек маршрута и типов данных, чтобы средний темп и продолжительность оставались точными.
  • Протестируйте длительные сеансы. Отслеживайте расход заряда батареи во время сеансов, длящихся несколько часов, чтобы убедиться, что интервал пакетной обработки и использование датчиков не разряжают устройство.
  • Согласуйте временные метки с частотой датчиков. Сопоставьте временные метки записей с фактической частотой датчиков, чтобы сохранить высокую точность данных.

Тестирование

Чтобы проверить правильность данных и удобство для пользователей, следуйте этим стратегиям тестирования и ознакомьтесь с официальной документацией по тестированию основных сценариев использования.

Инструменты проверки

  • Инструменты Здоровья и спорта: Это сопутствующее приложение позволяет вручную проверять записи, удалять тестовые данные и симулировать изменения в базе данных. Это лучший способ убедиться, что ваши записи хранятся правильно.
  • Модульное тестирование с помощью FakeHealthConnectClient. Используйте библиотеку тестирования, чтобы проверить, как приложение обрабатывает пограничные случаи, например отзыв разрешений или исключения API, без физического устройства.

Контрольный список для проверки качества

Типичная архитектура

Реализация тренировки обычно включает в себя:

Компонент Управляет
Контроллер сеансов Состояние сеанса
Таймер
Логика пакетной обработки
Контроллеры типов данных
Выборка местоположений
Уровень репозитория (содержит операции платформы "Здоровье и спорт") Вставка сеанса
Вставка типов данных
Вставка точек маршрута
Чтение сводок сеансов
Уровень интерфейса (дисплеи): Продолжительность
Типы данных в реальном времени
Предварительный просмотр карты
Расчеты по отрезкам
GPS-трек в реальном времени

Устранение неполадок

Проблема Возможные причины Разрешение
Маршрут не связан с сеансом Несовпадение идентификатора сеанса или диапазона дат. Убедитесь, что диапазон дат в ExerciseRoute полностью укладывается в период действия ExerciseSessionRecord. Если вы будете ссылаться на сеанс позже, убедитесь, что вы используете согласованные идентификаторы. Подробнее о записи маршрутов тренировок…
Отсутствуют типы данных (например, пульс). Отсутствуют разрешения на запись или неправильно настроены фильтры времени. Убедитесь, что вы запросили разрешение на доступ к определенному типу данных и пользователь его предоставил. Убедитесь, что в ReadRecordsRequest используется TimeRangeFilter, соответствующий сеансу. Подробнее о разрешениях…
Не удается записать сеанс Временные метки пересекаются. Платформа "Здоровье и спорт" может отклонять записи, которые пересекаются с существующими данными из того же приложения. Убедитесь, что startTime нового сеанса наступает после endTime предыдущего.
Данные GPS не записаны Активная служба была закрыта или неактивна. Чтобы собирать данные, когда экран выключен, необходимо использовать активную службу с атрибутом foregroundServiceType="health" или атрибутом местоположения.
Появляются повторяющиеся записи Не указан атрибут clientRecordId. Укажите уникальное значение clientRecordId в поле Metadata каждой записи. Это позволяет платформе "Здоровье и спорт" удалять дубликаты, если одни и те же данные записываются дважды во время повторной попытки синхронизации. Подробнее о рекомендациях…

Общие рекомендации по отладке

Проверьте статус разрешения. Всегда вызывайте getPermissionStatus() перед попыткой чтения или записи. Пользователи могут в любой момент отозвать разрешения в системных настройках.
Проверьте режим выполнения. Если ваше приложение не собирает данные в фоновом режиме, убедитесь, что вы указали правильные разрешения в файле AndroidManifest.xml и что пользователь не перевел приложение в режим ограничения энергопотребления.