Wear OS 기반 웨어러블 기기로 앱을 확장하여 앱의 건강 및 피트니스 환경을 개선하세요.
Wear OS 모듈 추가
Android 스튜디오는 앱에 Wear OS 모듈을 추가하는 편리한 마법사를 제공합니다. File > New Module 메뉴에서 다음 이미지와 같이 Wear OS를 선택합니다.
최신 버전의 헬스 서비스를 사용하려면 최소 SDK가 API 30 이상이어야 합니다. 건강 관리 서비스는 건강 센서를 자동으로 구성하여 측정항목을 추적하고 데이터를 기록하는 작업을 더 쉽게 해줍니다.
마법사를 완료한 후 프로젝트를 동기화합니다. 다음과 같은 실행 구성이 표시됩니다.
이렇게 하면 웨어러블 기기에서 Wear OS 모듈을 실행할 수 있습니다. 다음과 같은 옵션을 선택할 수 있습니다.
구성을 실행하면 앱이 Wear OS 에뮬레이터 또는 기기에 배포되고 'hello world' 환경이 표시됩니다. 앱을 시작하기 위한 Wear OS용 Compose를 사용하는 기본 UI 설정입니다.
Health Services 및 Hilt 추가
다음 라이브러리를 Wear OS 모듈에 통합합니다.
Health Services Manager 만들기
건강 관리 서비스를 좀 더 편리하게 사용하고 더 작고 원활한 API를 노출하려면 다음과 같은 래퍼를 만들면 됩니다.
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()
}
다음 스니펫을 사용하여 Hilt 모듈을 만들어 관리합니다.
@Module
@InstallIn(SingletonComponent::class)
internal object DataModule {
@Provides
@Singleton
fun provideHealthServices(@ApplicationContext context: Context): HealthServicesManager = HealthServicesManager(context)
}
HealthServicesManager를 다른 Hilt 종속 항목으로 삽입할 수 있습니다.
새로운 HealthServicesManager는 심박수 모니터의 리스너를 등록하고 수신된 데이터를 내보내는 heartRateMeasureFlow() 메서드를 제공합니다.
웨어러블 기기에서 데이터 업데이트 사용 설정
피트니스 관련 데이터를 업데이트하려면 BODY_SENSORS 권한이 필요합니다. 아직 하지 않았다면 앱의 매니페스트 파일에서 BODY_SENSORS 권한을 선언합니다. 그런 다음 이 스니펫에 표시된 대로 권한을 요청합니다.
val permissionState = rememberPermissionState(
permission = Manifest.permission.BODY_SENSORS,
onPermissionResult = { granted -> /* do something */ }
)
[...]
if (permissionState.status.isGranted) {
// do something
} else {
permissionState.launchPermissionRequest()
}
실제 기기에서 앱을 테스트하면 데이터가 업데이트되기 시작합니다.
Wear OS 4부터는 에뮬레이터에도 테스트 데이터가 자동으로 표시됩니다. 이전 버전에서는 센서의 데이터 스트림을 시뮬레이션할 수 있습니다. 터미널 창에서 다음 ADB 명령어를 실행합니다.
adb shell am broadcast \
-a "whs.USE_SYNTHETIC_PROVIDERS" \
com.google.android.wearable.healthservices
다양한 심박수 값을 확인하려면 다양한 운동을 시뮬레이션해 보세요. 이 명령어는 걷기를 시뮬레이션합니다.
adb shell am broadcast \
-a "whs.synthetic.user.START_WALKING" \
com.google.android.wearable.healthservices
이 명령어는 다음을 실행하는 것을 시뮬레이션합니다.
adb shell am broadcast \
-a "whs.synthetic.user.START_RUNNING" \
com.google.android.wearable.healthservices
데이터 시뮬레이션을 중지하려면 다음 명령어를 실행합니다.
adb shell am broadcast -a \
"whs.USE_SENSOR_PROVIDERS" \
com.google.android.wearable.healthservices
심박수 데이터 읽기
BODY_SENSORS 권한이 부여되면 HealthServicesManager에서 사용자의 심박수(heartRateMeasureFlow())를 읽을 수 있습니다. Wear OS 앱의 UI에는 웨어러블 기기의 센서로 측정된 현재 심박수 값이 표시됩니다.
ViewModel에서 다음 스니펫과 같이 심박수 흐름 객체를 사용하여 데이터 수집을 시작합니다.
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
}
}
다음과 유사한 컴포저블 객체를 사용하여 앱의 UI에 라이브 데이터를 표시합니다.
val heartRate by viewModel.hr
Text(
text = "Heart Rate: $heartRate",
style = MaterialTheme.typography.display1
)
휴대기기로 데이터 전송
건강 및 피트니스 데이터를 휴대기기로 전송하려면 건강 관리 서비스에서 DataClient 클래스를 사용하세요. 다음 코드 스니펫은 앱에서 이전에 수집한 심박수 데이터를 전송하는 방법을 보여줍니다.
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")
}
}
}
휴대전화에서 데이터 수신
휴대전화에서 데이터를 수신하려면 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
}
}
}
}
}
이 단계를 완료하면 다음과 같은 몇 가지 흥미로운 세부정보를 확인할 수 있습니다.
@AndroidEntryPoint주석을 사용하면 이 클래스에서 Hilt를 사용할 수 있습니다.@Inject lateinit var heartRateMonitor: HeartRateMonitor은 실제로 이 클래스에 종속 항목을 삽입합니다.- 이 클래스는
onDataChanged()를 구현하고 파싱하여 사용할 수 있는 이벤트 컬렉션을 수신합니다.
다음 HeartRateMonitor 로직을 사용하면 수신된 심박수 값을 앱 코드베이스의 다른 부분으로 전송할 수 있습니다.
class HeartRateMonitor {
private val datapoints = MutableSharedFlow<Int>(extraBufferCapacity = 10)
fun receive(): SharedFlow<Int> = datapoints.asSharedFlow()
fun send(hr: Int) {
datapoints.tryEmit(hr)
}
}
데이터 버스는 onDataChanged() 메서드에서 이벤트를 수신하고 SharedFlow를 사용하여 데이터 관찰자에게 이벤트를 제공합니다.
마지막 부분은 휴대전화 애플리케이션 AndroidManifest.xml에서 Service를 선언하는 것입니다.
<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>
휴대용 기기에 실시간 데이터 표시
휴대기기에서 실행되는 앱 부분에서 HeartRateMonitor를 뷰 모델의 생성자에 삽입합니다. 이 HeartRateMonitor 객체는 심박수 데이터를 관찰하고 필요에 따라 UI 업데이트를 내보냅니다.