<br />

> This guide is compatible with Health Connect version [1.1.0](https://developer.android.com/jetpack/androidx/releases/health-connect.md.txt#1.1.0) or later.

<br />

If you're looking to build a workout experience in your app, you can use Health
Connect to do things like:

- Write exercise sessions
- Write workout routes
- Write workout metrics such as heart rate, speed, and distance
- Read workout data from other apps

This guide describes how to build these workout features, covering data types,
background execution, permissions, recommended workflows, and best practices.

## Overview: Building a Comprehensive Workout Tracker

You can build a comprehensive workout tracking experience using Health Connect
by following these core steps:

- Correctly implementing permissions based on Health Permissions.
- Recording sessions using [`ExerciseSessionRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/ExerciseSessionRecord.md.txt).
- Writing workout data consistently during the session.
- Managing background execution properly to verify continuous data capture.
- Reading session data for post-workout summaries and analysis.

This workflow enables interoperability with other Health Connect apps and
verifies user-controlled data access.

## Before you begin

Before implementing workout features:

- [Integrate Health Connect](https://developer.android.com/health-and-fitness/health-connect/get-started.md.txt#step-1) using the appropriate dependency.
- [Create a `HealthConnectClient`](https://developer.android.com/health-and-fitness/health-connect/get-started.md.txt#step-2) instance.
- Verify your app implements runtime [permission flows based on Health Permissions](https://developer.android.com/health-and-fitness/health-connect/get-started.md.txt#declare-permissions).
- If your workflow uses GPS, set up location permission and a foreground service.

## Key concepts

Health Connect represents workout data using a few core components. An
`ExerciseSessionRecord` acts as the central record for a workout,
containing details like start or end times and exercise type. During a
session, various data types such as `HeartRateRecord` or `SpeedRecord` can
be recorded. For outdoor activities, `ExerciseRoute` stores GPS data, which
is linked to its corresponding session.

### Exercise sessions

`ExerciseSessionRecord` is the central record for workout data, representing a
single workout session. Each record stores:

- `startTime`
- `endTime`
- `exerciseType`
- Optional session metadata (title, notes)

An `ExerciseSessionRecord` can also contain exercise routes, laps, and
segments as part of its data. In addition, other data types such as
`HeartRateRecord` or `SpeedRecord` can be recorded during a session and
associated with it.

### Associated data types

Data associated with workout sessions is represented by individual record
types. Common types include:

- [`HeartRateRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/HeartRateRecord.md.txt): Represents a series of heart rate measurements.
- [`SpeedRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/SpeedRecord.md.txt): Represents a series of speed measurements.
- [`DistanceRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/DistanceRecord.md.txt): Represents distance covered between readings.
- [`TotalCaloriesBurnedRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/TotalCaloriesBurnedRecord.md.txt): Represents total calories burned between readings.
- [`ElevationGainedRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/ElevationGainedRecord.md.txt): Represents elevation gained between readings.
- [`StepsCadenceRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/StepsCadenceRecord.md.txt): Represents steps cadence between readings.
- [`PowerRecord`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/records/PowerRecord.md.txt): Represents power output between readings, common in activities like cycling.

For a complete list of data types, see
[Health Connect data types](https://developer.android.com/health-and-fitness/health-connect/data-types.md.txt).

### Exercise routes

You can associate a route with outdoor workouts using `ExerciseRoute`. Routes
consist of sequential `ExerciseRoute.Location` objects each containing:

- Latitude and longitude
- Optional altitude
- Optional bearing
- Accuracy information
- Timestamp

### Link session routes

An `ExerciseRoute` contains the sequential location data for an exercise
session. It isn't treated as an independent record in Health Connect. Instead,
you provide `ExerciseRoute` data when inserting or updating an
`ExerciseSessionRecord`.

## Development considerations

Workout tracking apps often need to run for extended periods, frequently
in the background when the screen is off. When building your workout
features, it's important to consider how to manage background execution
and request the necessary permissions for workout data.

### Background execution

Workout apps commonly run with the screen off. When in this state, you should
use:

- Foreground services for location and sensor sampling
- [`WorkManager`](https://developer.android.com/reference/androidx/work/WorkManager.md.txt) for deferred write or syncing
- Batching strategies for regular record write

Maintain continuity by keeping session ID consistent across all writes.

### Permissions

Your app must request the relevant Health Connect permissions before reading or
writing workout data. Common permissions for workouts
include exercise sessions, exercise routes, and metrics like heart rate or
speed. This includes the following:

- **Exercise sessions:** Read and write permissions for `ExerciseSessionRecord`.
- **Exercise routes:** Read and write permissions for `ExerciseRoute`.
- **Heart rate:** Read and write permissions for `HeartRateRecord`.
- **Speed:** Read and write permissions for `SpeedRecord`.
- **Distance:** Read and write permissions for `DistanceRecord`.
- **Calories:** Read and write permissions for `TotalCaloriesBurnedRecord`.
- **Elevation Gained:** Read and write permissions for `ElevationGainedRecord`.
- **Steps Cadence:** Read and write permissions for `StepsCadenceRecord`.
- **Power:** Read and write permissions for `PowerRecord`.
- **Steps:** Read and write permissions for `StepsRecord`.

The following shows an example of how to request multiple permissions for a
workout session that includes route, heart rate, distance, calories, speed,
and steps data:

After creating a client instance, your app needs to request permissions from
the user. Users must be allowed to grant or deny permissions at any time.

To do so, create a set of permissions for the required data types.
Make sure that the permissions in the set are declared in your Android
manifest first.


```kotlin
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)
    )
```
Use [`getGrantedPermissions`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/PermissionController.md.txt#getGrantedPermissions()) to see if your app already has the required permissions granted. If not, use [`createRequestPermissionResultContract`](https://developer.android.com/reference/kotlin/androidx/health/connect/client/PermissionController.md.txt#createRequestPermissionResultContract(kotlin.String)) to request those permissions. This displays the Health Connect permissions screen.

```kotlin
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.") }
    }
}
```
Because users can grant or revoke permissions at any time, your app needs to check for permissions every time before using them and handle scenarios where permission is lost.

<br />

To request permissions, call the `checkPermissionsAndRun` function:


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

<br />

If you only need to request permissions for a single data type, such as heart
rate, include only that data type in your permissions set:

Access to heart rate is protected by the following permissions:

- `android.permission.health.READ_HEART_RATE`
- `android.permission.health.WRITE_HEART_RATE`

To add heart rate capability to your app, start by requesting
permissions for the `HeartRateRecord` data type.

Here's the permission you need to declare to be able to write
heart rate:

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

To read heart rate, you need to request the following permissions:

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

## Implement a workout session

This section describes the recommended workflow for recording workout data.

### Start the session

To create a new workout:

1. Generate a unique session ID: Verify this ID is stable. If your app process is killed and restarted, you must be able to resume using the same ID to prevent fragmented sessions.
2. Set a `metadata.clientRecordId` to prevent duplicates during sync retries.
3. Write an `ExerciseSessionRecord`: Include the start time.
4. Start collecting Data type and GPS data: Only start these after the session record is successfully initialized.

Example:


```kotlin
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))
```

<br />

### Record exercise routes

To learn more about reading guidance see [Read raw data](https://developer.android.com/health-and-fitness/health-connect/read-data.md.txt).

When recording an exercise route, you should batch your data. This means instead
of saving every single GPS point as it happens, you collect a group of points
and save them all at once in a single call.

This is important because every time your app reads or writes to Health Connect,
it uses a tiny bit of battery and processing power.

> [!NOTE]
> **Best Practice:** Data Integrity: Before batching, validate your GPS accuracy. Filter out points with high horizontal accuracy radius to verify a clean route. You can verify this logic using [Unit tests](https://developer.android.com/health-and-fitness/health-connect/test/unit-tests.md.txt) to simulate "noisy" GPS data.

The following code shows how to record in batches:

```kotlin
// 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))
```

### End a session

After stopping data collection:

- Update the record: Your app updates `ExerciseSessionRecord` with an `endTime`.
- Finalize data: Optionally compute summary values (like total distance or average pace) and write them as additional records.

> [!NOTE]
> **Best Practice:** Avoid Overlaps Before finalizing, verify that this session's **endTime** does not overlap with the **startTime** of any subsequent records. Overlapping sessions are a common cause of write failures. [See Troubleshooting](https://developer.android.com/health-and-fitness/health-connect/experiences/workouts.md.txt#troubleshooting) for more.

```kotlin
val finishedSession = session.copy(endTime = Instant.now())
healthConnectClient.updateRecords(listOf(finishedSession))
```

## Reading workout data

Apps can read exercise sessions and their associated data to summarize activity,
provide health insights, or sync data with an external server. For example, you
can read an `ExerciseSessionRecord` and then query the `HeartRateRecord` or
`DistanceRecord` that occurred during that same time interval.

If you need to sync workout data with a backend server, or keep your app's
datastore up-to-date with Health Connect, use ChangeLogs. This lets you retrieve
a list of inserted, updated, or deleted records since a specific point in time,
which is more efficient than manually tracking changes or repeatedly reading all
data. For more information, see [Sync data with Health Connect](https://developer.android.com/health-and-fitness/health-connect/sync-data.md.txt).

### Read sessions

To read exercise sessions, use a `ReadRecordsRequest` with
`ExerciseSessionRecord` as the type. You usually filter this by a specific time
range.

```kotlin
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
    }
}
```

### Read routes

Although `ExerciseRoute` data is written as part of an exercise session, it
must be read separately. Use the `getExerciseRoute()` method with the session's
ID to read its route data:

```kotlin
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
        }
    }
}
```

### Read data types

To read specific granular data (like heart rate) that occurred during a session,
use the session's `startTime` and `endTime` to filter the request for that data
type.

```kotlin
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
        }
    }
}
```

## Best practices

Follow these guidelines to improve data reliability and user experience:

- **Write frequently during active tracking**: For active tracking, write data as it becomes available or at a maximum interval of 15 minutes.
- **Use WorkManager for background syncs** : Use [`WorkManager`](https://developer.android.com/reference/androidx/work/WorkManager.md.txt) for deferred writes. Aim for a 15-minute interval to strike a balance between real-time data and battery efficiency.
- **Batch write requests**: Don't write every single sensor event individually. Chunk your requests. Health Connect handles up to 1000 records per write request.
- **Keep session IDs stable and unique**: Use consistent identifiers for your sessions. If a session is edited or updated, using the same ID prevents it from being treated as a new, separate session.
- **Use batching for both data types and route points** : To reduce Input/Output overhead and preserve battery life, group your data points into a single `insertRecords` call rather than writing each point individually.
- **Avoid writing duplicate data: Use Client IDs** : When creating records, set a `metadata.clientRecordId`. Health Connect uses this to identify unique records. If you attempt to write a record with a `clientRecordId` that already exists, Health Connect will ignore the duplicate or update the existing record rather than creating a new one. Setting a `metadata.clientRecordId` is the most effective way to prevent duplicates during sync retries or app reinstalls.  

  ```kotlin
  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"
      )
  )
  ```
- **Check existing data**: Before syncing, query the time range to see if records from your app already exist.
- **Validate GPS accuracy** : Filter out low-accuracy GPS samples (For example, points with a high horizontal accuracy radius) before writing to the `ExerciseRoute` to verify the map looks clean and professional.
- **Ensure timestamps do not overlap**: Verify that a new session does not start before the previous one ends. Overlapping sessions can cause conflicts in fitness dashboards and summary calculations.
- **Provide clear rationales for permission** : Use the `Permission.createIntent` flow to explain why your app needs access to health data, for example: 'To monitor your blood pressure trends and provide insights.'
- **Support pause and resume**: Verify your app handles pauses correctly. When a user pauses, stop collecting route points and data types so that the average pace and duration remain accurate.
- **Test long-running sessions**: Monitor battery consumption during sessions lasting several hours to verify your batching interval and sensor usage don't drain the device.
- **Align timestamps with sensor rates**: Match your record timestamps to the actual frequency of your sensors to maintain data high-fidelity.

## Testing

To verify data correctness and a high-quality user experience, follow these
testing strategies and refer to the official [Test top use cases](https://developer.android.com/health-and-fitness/health-connect/test/test-cases.md.txt)
documentation.

### Verification tools

- **[Health Connect Toolbox](https://developer.android.com/health-and-fitness/health-connect/test/health-connect-toolbox.md.txt):** Use this companion app to manually inspect records, delete test data, and simulate changes to the database. It is the best way to verify that your records are being stored correctly.
- **[Unit testing with `FakeHealthConnectClient`](https://developer.android.com/health-and-fitness/health-connect/test/unit-tests.md.txt):** Use the testing library to verify how your app handles edge cases, like permission revocation or API exceptions without needing a physical device.

### Quality checklist

**Confirm records in Health Connect:** Open the Health Connect app and navigate to Data and access to verify records appear with expected values. **Read data from other apps:** Test your app's ability to read sessions written by other apps to verify ecosystem compatibility. See [Reading workout data](https://developer.android.com/health-and-fitness/health-connect/experiences/workouts.md.txt#reading-workout-data). **Verify route data on a map:** Render the coordinates from your `ExerciseRoute` using a map library to check for continuity and remove "zig-zag" artifacts from low-accuracy points. **Balance write frequency:** Monitor battery usage. Frequent writes provide high detail but increase drain.

## Typical architecture

A workout implementation commonly includes:

| Component | Manages |
|---|---|
| Session controller | Session state Timer Batching logic Data types controllers Location sampling |
| Repository layer (wraps Health Connect operations:) | Insert session Insert data types Inserts route points Read session summaries |
| UI Layer (Displays): | Duration Live data types Map preview Split calculations Live GPS Trace |

## Troubleshooting

<br />

| Symptom | Possible cause | Resolution |
|---|---|---|
| Route not associated with session | Session ID or time range mismatch. | Verify the `ExerciseRoute` is written with a time range that falls entirely within the `ExerciseSessionRecord` duration. Verify you are using consistent IDs if referencing the session later. See [Recording exercise routes](https://developer.android.com/health-and-fitness/health-connect/experiences/workouts.md.txt#recording-exercise-routes). |
| Missing data types (for example, Heart Rate) | Missing write permissions or incorrect time filters. | Check that you have requested and the user has granted the specific data type permission. Verify your `ReadRecordsRequest` uses a `TimeRangeFilter` that matches the session. See [Permissions](https://developer.android.com/health-and-fitness/health-connect/experiences/workouts.md.txt#permissions). |
| Session fails to write | Overlapping timestamps. | Health Connect may reject records that overlap with existing data from the same app. Validate that the `startTime` of a new session is after the `endTime` of the previous one. |
| No GPS data recorded | Foreground service was killed or inactive. | To collect data while the screen is off, you must use a [Foreground Service](https://developer.android.com/develop/background-work/services/fgs.md.txt) with the `foregroundServiceType="health"` or location attribute. |
| Duplicate records appear | Missing `clientRecordId`. | Assign a unique `clientRecordId` in the `Metadata` of each record. This allows Health Connect to perform de-duplication if the same data is written twice during a sync retry. See [Best practices](https://developer.android.com/health-and-fitness/health-connect/experiences/workouts.md.txt#best-practices). |

<br />

### Common debugging steps

<br />

|---|---|
| Check permission state. | Always call `getPermissionStatus()` before attempting a read or write operation. Users can revoke permissions in system settings at any time. |
| Verify execution mode. | If your app is not collecting data in the background, verify that you have declared the correct permissions in your `AndroidManifest.xml` file and that the user hasn't placed the app into "Battery Restricted" mode. |

<br />