Mejora la experiencia de salud y fitness de tu app expandiéndola a dispositivos wearables con Wear OS.
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:
Es importante tener en cuenta que el SDK mínimo debe ser 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 las métricas y el registro de datos, ya que configuran automáticamente los sensores de salud.
Después de completar el asistente, sincroniza tu proyecto. Aparecerá la siguiente configuración de Run:
Esto te permite ejecutar el módulo de Wear OS en un dispositivo wearable. Tienes dos opciones:
Ejecutar en un emulador
Ejecuta la app en un dispositivo real.
Cuando ejecutas la configuración, se implementa la app en el emulador o dispositivo Wear OS y se muestra una experiencia de "Hola mundo". Esta es la configuración básica de la IU, con Compose para Wear OS, para comenzar a usar tu app.
Agrega Health Services y Hilt
Integra las siguientes bibliotecas en tu módulo de Wear OS:
- Servicios de salud: Facilita el acceso a los sensores y los datos del reloj, y lo hace de manera más eficiente en cuanto al consumo de energía.
- Hilt: Permite la inserción y administración eficaces de dependencias.
Crea el administrador de los Servicios de salud
Para que el uso de Health Services sea un poco más conveniente y exponer 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 de frecuencia cardíaca y emite los datos recibidos.
Habilita las actualizaciones de datos en dispositivos wearables
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 tu 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 versiones anteriores, puedes simular el flujo de datos del sensor. En una ventana de la terminal, ejecuta este comando de ADB:
adb shell am broadcast \
-a "whs.USE_SYNTHETIC_PROVIDERS" \
com.google.android.wearable.healthservices
Para ver diferentes valores de frecuencia cardíaca, intenta simular diferentes ejercicios. Este comando simula una caminata:
adb shell am broadcast \
-a "whs.synthetic.user.START_WALKING" \
com.google.android.wearable.healthservices
Este comando simula la ejecución de lo siguiente:
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 frecuencia cardíaca
Con el permiso BODY_SENSORS otorgado, puedes leer el ritmo cardíaco del usuario (heartRateMeasureFlow()) en 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 portátil
Para enviar datos de salud y actividad física a un dispositivo portátil, usa la clase DataClient en los Servicios de salud. En el siguiente fragmento de código, se muestra cómo enviar los datos de frecuencia cardíaca que tu app recopiló 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 objeto 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
}
}
}
}
}
Cuando completes este paso, observa algunos detalles interesantes:
- La anotación
@AndroidEntryPointnos permite usar Hilt en esta clase. @Inject lateinit var heartRateMonitor: HeartRateMonitorinyectará 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 a través de 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 portátil, inyecta 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.