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. - 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 using the appropriate dependency.
- Create a
HealthConnectClientinstance. - Verify your app implements runtime permission flows based on Health 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:
startTimeendTimeexerciseType- 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: Represents a series of heart rate measurements.SpeedRecord: Represents a series of speed measurements.DistanceRecord: Represents distance covered between readings.TotalCaloriesBurnedRecord: Represents total calories burned between readings.ElevationGainedRecord: Represents elevation gained between readings.StepsCadenceRecord: Represents steps cadence between readings.PowerRecord: Represents power output between readings, common in activities like cycling.
For a complete list of data types, see Health Connect data types.
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
WorkManagerfor 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.
// Create a set of permissions for required data types
val PERMISSIONS =
setOf(
HealthPermission.getReadPermission(ExerciseSessionRecord::class),
HealthPermission.getWritePermission(ExerciseSessionRecord::class),
HealthPermission.getReadPermission(ExerciseRoute::class),
HealthPermission.getWritePermission(ExerciseRoute::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 to see if your app already has the
required permissions granted. If not, use
createRequestPermissionResultContract to request
those permissions. This displays the Health Connect permissions screen.
// Create the permissions launcher
val requestPermissionActivityContract = PermissionController.createRequestPermissionResultContract()
val requestPermissions = registerForActivityResult(requestPermissionActivityContract) { granted ->
if (granted.containsAll(PERMISSIONS)) {
// Permissions successfully granted
} else {
// Lack of required permissions
}
}
suspend fun checkPermissionsAndRun(healthConnectClient: HealthConnectClient) {
val granted = healthConnectClient.permissionController.getGrantedPermissions()
if (granted.containsAll(PERMISSIONS)) {
// Permissions already granted; proceed with inserting or reading data
} else {
requestPermissions.launch(PERMISSIONS)
}
}
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.
To request permissions, call the checkPermissionsAndRun function:
if (!granted.containsAll(PERMISSIONS)) {
requestPermissions.launch(PERMISSIONS)
// Check if required permissions are not granted, and return
}
// Permissions already granted; proceed with inserting or reading data
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_RATEandroid.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:
<application>
<uses-permission
android:name="android.permission.health.WRITE_HEART_RATE" />
...
</application>
To read heart rate, you need to request the following permissions:
<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:
- 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.
- Set a
metadata.clientRecordIdto prevent duplicates during sync retries. - Write an
ExerciseSessionRecord: Include the start time. - Start collecting Data type and GPS data: Only start these after the session record is successfully initialized.
Example:
val sessionId = UUID.randomUUID().toString()
val sessionClientId = UUID.randomUUID().toString()
val session = ExerciseSessionRecord(
id = sessionId,
exerciseType = ExerciseType.EXERCISE_TYPE_RUNNING,
startTime = Instant.now(),
endTime = null,
metadata = Metadata(clientRecordId = sessionClientId),
)
healthConnectClient.insertRecords(listOf(session))
Record exercise routes
To learn more about reading guidance see Read raw data.
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.
The following code shows how to record in batches:
// 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
ExerciseSessionRecordwith anendTime. - Finalize data: Optionally compute summary values (like total distance or average pace) and write them as additional records.
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.
Read sessions
To read exercise sessions, use a ReadRecordsRequest with
ExerciseSessionRecord as the type. You usually filter this by a specific time
range.
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:
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.
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 Frequency
- Active Tracking(Foreground): For active workouts, write data as it becomes available or at a maximum interval of 15 minutes.
- Background Sync: Use
WorkManagerfor deferred writes. Aim for a 15-minute interval to strike a balance between real-time data and battery efficiency. - Batching: Do not 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 workout.
- 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
insertRecordscall 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 aclientRecordIdthat already exists, Health Connect will ignore the duplicate or update the existing record rather than creating a new one. Setting ametadata.clientRecordIdis the most effective way to prevent duplicates during sync retries or app reinstalls.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
ExerciseRouteto 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.createIntentflow to explain why your app needs access to health data, such as "To map your runs and calculate calorie burn."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 (For example, 1Hz for GPS) 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 documentation.
Verification tools
- Health Connect Toolbox: 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: 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
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
| 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. |
| 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. |
| 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 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. |
Common debugging steps
- 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 you have declared the correct permissions in your
AndroidManifest.xmland that the user hasn't placed the app into "Battery Restricted" mode.