Synchronizing data

Most apps that integrate with Health Connect have their own datastore. Health Connect provides ways to keep your app's datastore in sync with Health Connect's datastore.

Your app's datastore should be the source of truth, and should stay in sync with data in Health Connect.

Make sure your app does the following:

  • Feed new or updated data from your app's datastore to Health Connect.
  • Consume new or updated data from Health Connect.
  • Delete data from Health Connect when it is deleted in your app's datastore.

In each case, ensure that the feeding or ingestion process keeps both Health Connect and your app's datastore in sync.

Metadata in Health Connect Records

To help with each of the above, it is first worth examining the Metadata class. On creation, each Record in Health Connect has a metadata field. The following properties are relevant to synchronization:

Properties
Stringid
Every `Record` in Health Connect has a unique `id` value. This is automatically assigned when inserting a new record
InstantlastModifiedTime
Every `Record` also keeps track of the last time the record was modified. This is automatically populated.
String?clientRecordId
Each `Record` can have a unique ID associated with it to serve as a key back into your app’s datastore. This value is controlled by your app.
LongclientRecordVersion
Where a record has `clientRecordId`, the `clientRecordVersion` is used to allow for data to stay in sync with the versioning in your app’s datastore. This value is controlled by your app.

Feeding data to Health Connect from your app's datastore

Preparation

Entries in your app's own datastore should have the following elements:

  • A unique key, such as a UUID.
  • A version or timestamp.

Design your app datastore to keep track of what data has already been fed to Health Connect. To achieve this, you can try the following:

  • Provide a change log and a token that can be used to retrieve only records since that token.
  • Track the last modified time of exported data.

This is essential to ensure that only new or updated data is fed to Health Connect.

Feeding data to Health Connect

  1. Obtain the list of new or updated entries from your app's datastore.
  2. For each entry, create a Record object of the appropriate type, such as WeightRecord.
  3. Specify a Metadata object with each Record using the unique key and version details from your app's own datastore:
val record = WeightRecord(
    metadata = Metadata(
        clientRecordId = "<Record unique ID from app>",
        clientRecordVersion = 12345678 // record version from app, could be a timestamp.
    ),
    weight = ...
)

val record = WeightRecord( metadata = Metadata( clientRecordId = "", clientRecordVersion = 12345678 // record version from app, could be a timestamp. ), weight = ... )

  1. For both new or updated entries from your app's datastore, use insertRecords() to feed the data to Health Connect.

Insert vs update

Using the above approach, Health Connect checks whether any existing records in Health Connect already have the given clientRecordId

  • If Yes, then Health Connect compares the value of clientRecordVersion, and the Record in Health Connect is updated if the new clientRecordVersion value is greater.
  • If No, then the data is inserted as a new Record.

This approach allows the developer to ensure that records in your app’s own datastore are represented only once in Health Connect, while still remaining updatable.

Practical considerations for writing

  • Apps should only write own-sourced data to Health Connect: If data in your app has been imported from another app, then it should be the responsibility of that other app to write its own data to Health Connect.
  • Where you are writing a lot of data, perform the writes in batches, for example 1000 records at a time.
  • Ensure there is logic in place to handle write exceptions, for example data that is outside of bounds, or an internal system error:
    • WorkManager offers backoff and retry strategies.
    • If writing to Health Connect is ultimately unsuccessful, ensure that your app can move past that point of export.
    • Log and report errors to aid diagnosis.

Consuming data from Health Connect

Health Connect offers the Changes Sync API to allow apps to consume new or updated data from Health Connect, or to be notified of data that has been deleted in Health Connect.

App datastore structure

Your app’s own datastore needs to store the Health Connect id for each consumed record: This allows your app to determine whether each incoming change requires a new record to be created, or whether an existing record in your app’s own datastore should be updated.

You can use the Changes Sync API without storing the id in your datastore, but this results in duplicate records in your app when records are updated in Health Connect.

Registration

To receive a list of changes to consume into your apps datastore, your app is required to keep track of a Changes Token.

Supplying this Changes Token to Health Connect returns both a list of changes, and also a new Changes Token, for use next time.

To obtain a Changes Token, call getChangesToken, supplying the required data types:

val changesToken = healthConnectClient.getChangesToken(
    ChangesTokenRequest(recordTypes = setOf(StepsRecord::class))
)

Obtaining changes

  1. Call getChanges() using the token stored by your app, to obtain a list of changes - if any - that are available:

    suspend fun processChanges(token: String): String {
        var nextChangesToken = token
        do {
            val response = healthConnectClient.getChanges(nextChangesToken)
            response.changes.forEach { change ->
                when (change) {
                    is UpsertionChange -> processUpsertionChange(change)
                    is DeletionChange -> processDeletionChange(change)
                }
            }
            nextChangesToken = response.nextChangesToken
        } while (response.hasMore)
        // Return and store the changes token for use next time.
        return nextChangesToken
    }
    
  2. Filter the resulting changes, using the dataOrigin field of the metadata associated with each record, to remove any changes from the calling app. This is important to ensure you are not re-importing data that you have just written.

    val filteredChanges = changesResponse.changes.filter { change ->
    (change is UpsertionChange &&
        change.record.metadata.dataOrigin.packageName != context.packageName) ||
        change is DeletionChange
    }.toList()
    
  3. Write any changes to your local datastore

    1. The metadata field on each Record contains both a id that uniquely identifies this data in Health Connect and a lastModifiedTime. You should use this to determine whether to insert a record into your app’s datastore, or update an existing one. To do this, each entry in your app’s datastore needs to be able to store the Health Connect id.
  4. Use ChangesResponse.hasMore to check whether there are more changes to retrieve.

  5. If there are, use getChanges() with nextChangeToken, and repeat.

  6. If there are no more changes, store nextChangeToken for the next time an import is run.

Practical considerations

  • Token expiry - The Changes Token can expire if it is not used within a 90 days, so your app must have a strategy for how to sync data in that case.
    • One approach could be to do the following:
      1. Search your app's datastore for the most recently consumed record that also has an id from Health Connect.
      2. Request records from Health Connect from this timestamp forward, and insert/update in your app's own datastore.
      3. Request a Changes Token for use next time.
  • Foreground reads - Apps can only read data from Health Connect while they are in the Foreground. Whenn syncing data from Health Connect, be aware that access to Health Connect may be interrupted at any point. For example, if reading a large amount of data from Health Connect, your app should be able to handle an interruption midway through this sync, and continue it next time your app is opened.
  • Data type change tokens - If an app can consume more than one data type independently, use separate Changes Tokens for each data type. Only use a list of multiple data types with the Changes Sync API if these data types are always either consumed together or not at all.
  • Import timings - As your app cannot be notified of new data, it should check for new data at two points:
    1. Each time your app becomes active in the foreground, using lifecycle events.
    2. Periodically if your app remains in the foreground.
      • Where new data is available, this should be indicated to the user, allowing them to update the screen if they wish.

Deleting data from Health Connect

When a user deletes their own data from your app, you should ensure that this data is also removed from Health Connect. Use deleteRecords to do this. This takes a list of clientRecordIds, which makes it convenient to derive the list of records that need deletion.