Cómo integrar un módulo de Wear OS

Expande tu app a dispositivos wearable con la tecnología de Wear OS para mejorar la experiencia de salud y bienestar.

Cómo agregar un módulo de Wear OS

Android Studio proporciona un asistente práctico para agregar un módulo de Wear OS a tu app. En el menú File > New Module, selecciona Wear OS, como se muestra en la siguiente imagen:

Asistente de módulos de Wear OS en Android Studio
Figura 1: Cómo crear un módulo de Wear OS

Es importante tener en cuenta que el SDK mínimo debe ser el nivel de API 30 o superior para que puedas usar la versión más reciente de los Servicios de salud. Los Servicios de salud facilitan el seguimiento de métricas y el registro de datos mediante la configuración automática de sensores de salud.

Después de completar el asistente, sincroniza tu proyecto. Aparecerá la siguiente configuración de Run:

Una imagen que muestra el botón de ejecución de la app de Wear OS
Figura 2: Botón Ejecutar para el nuevo módulo de Wear OS

De esta manera, puedes ejecutar el módulo de Wear OS en un dispositivo wearable. Tienes dos opciones:

Cuando se ejecuta la configuración, se implementa la app en el emulador o dispositivo de Wear OS y se muestra la experiencia de "Hello World". Esta es la configuración básica de la IU con Compose para Wear OS, para comenzar a usar tu app.

Cómo agregar Servicios de salud y Hilt

Integra las siguientes bibliotecas en el módulo de Wear OS:

  • Servicios de salud: Hace que acceder a los sensores y los datos en el reloj sea muy conveniente y ahorres energía.
  • Hilt: Permite una inserción y administración eficaces de las dependencias.

Crea el administrador de Servicios de salud

Para que el uso de los Servicios de salud sea un poco más conveniente y expongas una API más pequeña y fluida, puedes crear un wrapper como este:

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()
}

Una vez que hayas creado el módulo de Hilt para administrarlo, usa el siguiente fragmento:

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

puedes insertar HealthServicesManager como cualquier otra dependencia de Hilt.

El nuevo HealthServicesManager proporciona un método heartRateMeasureFlow() que registra un objeto de escucha para el monitor cardíaco y emite los datos recibidos.

Cómo habilitar las actualizaciones de datos en dispositivos wearable

Las actualizaciones de datos relacionados con el entrenamiento requieren el permiso BODY_SENSORS. Si aún no lo hiciste, declara el permiso BODY_SENSORS en el archivo de manifiesto de tu app. Luego, solicita el permiso, como se muestra en este fragmento:

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

[...]

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

Si pruebas la app en un dispositivo físico, los datos deberían comenzar a actualizarse.

A partir de Wear OS 4, los emuladores también muestran datos de prueba automáticamente. En las versiones anteriores, puedes simular el flujo de datos del sensor. En una ventana de terminal, ejecuta este comando de ADB:

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

Si quieres ver diferentes valores de frecuencia cardíaca, prueba simular diferentes ejercicios. Este comando simula que caminas:

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

Este comando simula correr:

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

Para dejar de simular los datos, ejecuta este comando:

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

Leer datos de frec. cardíaca

Con el permiso BODY_SENSORS otorgado, puedes leer la frecuencia cardíaca del usuario (heartRateMeasureFlow()) en el HealthServicesManager. En la IU de la app para Wear OS, aparece el valor actual de la frecuencia cardíaca que mide el sensor del dispositivo wearable.

En tu ViewModel, comienza a recopilar datos con el objeto de flujo de frecuencia cardíaca, como se muestra en el siguiente fragmento:

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

Usa un objeto componible similar al siguiente para mostrar los datos en vivo en la IU de tu app:

val heartRate by viewModel.hr

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

Cómo enviar datos a un dispositivo de mano

Para enviar datos de salud y actividad física a un dispositivo de mano, usa la clase DataClient en los Servicios de salud. En el siguiente fragmento de código, se muestra cómo enviar datos de frecuencia cardíaca que recopiló tu app anteriormente:

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")
        }
    }
}

Cómo recibir los datos en el teléfono

Para recibir los datos en el teléfono, crea un 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
                }
            }
        }
    }
}

Al completar este paso, observa algunos detalles interesantes:

  • La anotación @AndroidEntryPoint nos permite usar Hilt en esta clase.
  • @Inject lateinit var heartRateMonitor: HeartRateMonitor insertará una dependencia en esta clase.
  • La clase implementa onDataChanged() y recibe una colección de eventos que puedes analizar y usar.

La siguiente lógica de HeartRateMonitor te permite enviar los valores de frecuencia cardíaca recibidos a otra parte de la base de código de tu app:

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

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

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

Un bus de datos recibe los eventos del método onDataChanged() y los pone a disposición de los observadores de datos que usan un SharedFlow.

El último bit es la declaración de Service en la aplicación para teléfonos 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>

Cómo mostrar datos en tiempo real en un dispositivo portátil

En la parte de tu app que se ejecuta en un dispositivo de mano, inserta el HeartRateMonitor en el constructor de tu modelo de vista. Este objeto HeartRateMonitor observa los datos de la frecuencia cardíaca y emite actualizaciones de la IU según sea necesario.