Integrate a Wear OS module

Enhance your app's health and fitness experience by expanding it onto wearable devices powered by Wear OS.

Add a Wear OS module

Android Studio provides a handy wizard to add a Wear OS module to your app. In the File > New Module menu, select Wear OS, as shown in the following image:

Wear OS module wizard in Android Studio
Figure 1: Create a Wear OS module

It's important to note that the Minimum SDK must be API 30 or higher to let you use the latest version of Health Services. Health Services makes it easier to track metrics and record data by configuring health sensors automatically.

After you complete the wizard, sync your project. The following Run configuration appears:

An image showing the Wear OS app run button
Figure 2: Run button for the new Wear OS module

This lets you run the Wear OS module on a wearable device. You have two options:

Running the configuration deploys the app to the Wear OS emulator or device and shows a "hello world" experience. This is the basic UI setup, using Compose for Wear OS, to get started with your app.

Add Health Services and Hilt

Integrate the following libraries into your Wear OS module:

  • Health Services: makes accessing sensors and data on the watch very convenient and more power-efficient.
  • Hilt: Allows for effective dependency injection and management.

Create the Health Services Manager

To make using Health Services a bit more convenient, and expose a smaller and smoother API, you can create a wrapper like this:

private const val TAG = "WATCHMAIN"

class HealthServicesManager(context: Context) {
    private val measureClient = HealthServices.getClient(context).measureClient

    suspend fun hasHeartRateCapability() = runCatching {
        val capabilities = measureClient.getCapabilities()
        (DataType.HEART_RATE_BPM in capabilities.supportedDataTypesMeasure)
    }.getOrDefault(false)

    /**
     * Returns a cold flow. When activated, the flow will register a callback for heart rate data
     * and start to emit messages. When the consuming coroutine is canceled, the measure callback
     * is unregistered.
     *
     * [callbackFlow] creates a  bridge between a callback-based API and Kotlin flows.
     */
    @ExperimentalCoroutinesApi
    fun heartRateMeasureFlow(): Flow<MeasureMessage> = callbackFlow {
        val callback = object : MeasureCallback {
            override fun onAvailabilityChanged(dataType: DeltaDataType<*, *>, availability: Availability) {
                // Only send back DataTypeAvailability (not LocationAvailability)
                if (availability is DataTypeAvailability) {
                    trySendBlocking(MeasureMessage.MeasureAvailability(availability))
                }
            }

            override fun onDataReceived(data: DataPointContainer) {
                val heartRateBpm = data.getData(DataType.HEART_RATE_BPM)
                Log.d(TAG, "💓 Received heart rate: ${heartRateBpm.first().value}")
                trySendBlocking(MeasureMessage.MeasureData(heartRateBpm))
            }
        }

        Log.d(TAG, "⌛ Registering for data...")
        measureClient.registerMeasureCallback(DataType.HEART_RATE_BPM, callback)

        awaitClose {
            Log.d(TAG, "👋 Unregistering for data")
            runBlocking {
                measureClient.unregisterMeasureCallback(DataType.HEART_RATE_BPM, callback)
            }
        }
    }
}

sealed class MeasureMessage {
    class MeasureAvailability(val availability: DataTypeAvailability) : MeasureMessage()
    class MeasureData(val data: List<SampleDataPoint<Double>>) : MeasureMessage()
}

Once you have created the Hilt module to manage it, using the following snippet:

@Module
@InstallIn(SingletonComponent::class)
internal object DataModule {
    @Provides
    @Singleton
    fun provideHealthServices(@ApplicationContext context: Context): HealthServicesManager = HealthServicesManager(context)
}

you can inject the HealthServicesManager as any other Hilt dependency.

The new HealthServicesManager provides a heartRateMeasureFlow() method that registers a listener for the heart monitor and emits the received data.

Enable data updates on wearable devices

Fitness-related data updates require the BODY_SENSORS permission. If you haven't done so already, declare the BODY_SENSORS permission in your app's manifest file. Then, request the permission, as shown in this snippet:

val permissionState = rememberPermissionState(
    permission = Manifest.permission.BODY_SENSORS,
    onPermissionResult = { granted -> /* do something */ }
)

[...]

if (permissionState.status.isGranted) {
    // do something
} else {
    permissionState.launchPermissionRequest()
}

If you test your app on a physical device, data should start updating.

Starting in Wear OS 4, emulators also show test data automatically. On previous versions, you can simulate the data stream from the sensor. In a terminal window, run this ADB command:

adb shell am broadcast \
-a "whs.USE_SYNTHETIC_PROVIDERS" \
com.google.android.wearable.healthservices

To see different heart rate values, try simulating different exercises. This command simulates walking:

adb shell am broadcast \
-a "whs.synthetic.user.START_WALKING" \
com.google.android.wearable.healthservices

This command simulates running:

adb shell am broadcast \
-a "whs.synthetic.user.START_RUNNING" \
com.google.android.wearable.healthservices

To stop simulating the data, run this command:

adb shell am broadcast -a \
"whs.USE_SENSOR_PROVIDERS" \
com.google.android.wearable.healthservices

Read heart rate data

With the BODY_SENSORS permission granted, you can read the user's heart rate (heartRateMeasureFlow()) in the HealthServicesManager. In the Wear OS app's UI, the current heart rate value appears, being measured by the sensor on the wearable device.

In your ViewModel, start collecting data using the heart rate flow object, as shown in the following snippet:

val hr: MutableState<Double> = mutableStateOf(0.0)

[...]

healthServicesManager
    .heartRateMeasureFlow()
    .takeWhile { enabled.value }
    .collect { measureMessage ->
        when (measureMessage) {
            is MeasureData -> {
                val latestHeartRateValue = measureMessage.data.last().value
                hr.value = latestHeartRateValue
            }

            is MeasureAvailability -> availability.value =
                    measureMessage.availability
        }
    }

Use a composable object similar to the following to display the live data in your app's UI:

val heartRate by viewModel.hr

Text(
  text = "Heart Rate: $heartRate",
  style = MaterialTheme.typography.display1
)

Send data to a handheld device

To send health and fitness data to a handheld device, use the DataClient class in Health Services. The following code snippet shows how to send heart rate data that your app previously collected:

class HealthServicesManager(context: Context) {
    private val dataClient by lazy { Wearable.getDataClient(context) }

[...]

    suspend fun sendToHandheldDevice(heartRate: Int) {
        try {
            val result = dataClient
                .putDataItem(PutDataMapRequest
                    .create("/heartrate")
                    .apply { dataMap.putInt("heartrate", heartRate) }
                    .asPutDataRequest()
                    .setUrgent())
                .await()

            Log.d(TAG, "DataItem saved: $result")
        } catch (cancellationException: CancellationException) {
            throw cancellationException
        } catch (exception: Exception) {
            Log.d(TAG, "Saving DataItem failed: $exception")
        }
    }
}

Receive the data on the phone

To receive the data on the phone, create a WearableListenerService:

@AndroidEntryPoint
class DataLayerListenerService : WearableListenerService() {

    @Inject
    lateinit var heartRateMonitor: HeartRateMonitor

    override fun onDataChanged(dataEvents: DataEventBuffer) {

        dataEvents.forEach { event ->
            when (event.type) {
                DataEvent.TYPE_CHANGED -> {
                    event.dataItem.run {
                        if (uri.path?.compareTo("/heartrate") == 0) {
                            val heartRate = DataMapItem.fromDataItem(this)
                                    .dataMap.getInt(HR_KEY)
                            Log.d("DataLayerListenerService",
                                    "New heart rate value received: $heartRate")
                            heartRateMonitor.send(heartRate)
                        }
                    }
                }

                DataEvent.TYPE_DELETED -> {
                    // DataItem deleted
                }
            }
        }
    }
}

Upon completion of this step, notice a few interesting details:

  • The @AndroidEntryPoint annotation lets us use Hilt in this class
  • The @Inject lateinit var heartRateMonitor: HeartRateMonitor will indeed inject a dependency in this class
  • The class implements onDataChanged() and receives a collection of events that you can parse and use

The following HeartRateMonitor logic lets you send the received heart rate values to another part of your app's codebase:

class HeartRateMonitor {
    private val datapoints = MutableSharedFlow<Int>(extraBufferCapacity = 10)

    fun receive(): SharedFlow<Int> = datapoints.asSharedFlow()

    fun send(hr: Int) {
        datapoints.tryEmit(hr)
    }
}

A data bus receives the events from the onDataChanged() method and makes them available to data observers using a SharedFlow.

The final bit is the declaration of the Service in the phone application AndroidManifest.xml:

<service
    android:name=".DataLayerListenerService"
    android:exported="true">
    <intent-filter>
        <!-- listeners receive events that match the action and data filters -->
        <action android:name="com.google.android.gms.wearable.DATA_CHANGED" />
        <data
            android:host="*"
            android:pathPrefix="/heartrate"
            android:scheme="wear" />
    </intent-filter>
</service>

Show real-time data on a handheld device

In the part of your app that runs on a handheld device, inject the HeartRateMonitor into your view model's constructor. This HeartRateMonitor object observes the heart rate data and emits UI updates as needed.