Tích hợp mô-đun Wear OS

Nâng cao trải nghiệm sức khoẻ và thể dục của ứng dụng bằng cách mở rộng ứng dụng sang các thiết bị đeo chạy Wear OS.

Thêm mô-đun Wear OS

Android Studio cung cấp một trình hướng dẫn tiện dụng để thêm mô-đun Wear OS vào ứng dụng của bạn. Trong trình đơn File > New Module (Tệp > Mô-đun mới), hãy chọn Wear OS, như minh hoạ trong hình ảnh sau:

Trình hướng dẫn mô-đun Wear OS trong Android Studio
Hình 1: Tạo một mô-đun Wear OS

Điều quan trọng cần lưu ý là SDK tối thiểu phải là API 30 trở lên để bạn có thể sử dụng phiên bản mới nhất của Dịch vụ y tế. Dịch vụ sức khoẻ giúp bạn dễ dàng theo dõi các chỉ số và ghi lại dữ liệu bằng cách tự động định cấu hình các cảm biến sức khoẻ.

Sau khi hoàn tất trình hướng dẫn, hãy đồng bộ hoá dự án của bạn. Cấu hình Run (Chạy) sau đây sẽ xuất hiện:

Hình ảnh minh hoạ nút chạy ứng dụng Wear OS
Hình 2: Nút Run (Chạy) cho mô-đun Wear OS mới

Nhờ đó, bạn có thể chạy mô-đun Wear OS trên thiết bị đeo. Bạn có hai tùy chọn:

Khi chạy cấu hình này, ứng dụng sẽ được triển khai đến trình mô phỏng hoặc thiết bị Wear OS và cho thấy trải nghiệm "xin chào thế giới". Đây là chế độ thiết lập giao diện người dùng cơ bản, sử dụng Compose cho Wear OS, để bắt đầu với ứng dụng của bạn.

Thêm Health Services và Hilt

Tích hợp các thư viện sau vào mô-đun Wear OS:

  • Dịch vụ sức khoẻ: giúp bạn truy cập vào các cảm biến và dữ liệu trên đồng hồ một cách thuận tiện và tiết kiệm pin hơn.
  • Hilt: Cho phép chèn và quản lý phần phụ thuộc một cách hiệu quả.

Tạo Trình quản lý Dịch vụ sức khoẻ

Để sử dụng Dịch vụ sức khoẻ một cách thuận tiện hơn và hiển thị một API nhỏ hơn và mượt mà hơn, bạn có thể tạo một trình bao bọc như sau:

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

Sau khi bạn tạo mô-đun Hilt để quản lý, hãy dùng đoạn mã sau:

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

bạn có thể chèn HealthServicesManager dưới dạng bất kỳ phần phụ thuộc Hilt nào khác.

HealthServicesManager mới cung cấp một phương thức heartRateMeasureFlow() để đăng ký trình nghe cho thiết bị theo dõi nhịp tim và phát dữ liệu nhận được.

Bật tính năng cập nhật dữ liệu trên thiết bị đeo

Để cập nhật dữ liệu liên quan đến hoạt động thể dục, bạn cần có quyền BODY_SENSORS. Nếu bạn chưa làm, hãy khai báo quyền BODY_SENSORS trong tệp kê khai của ứng dụng. Sau đó, hãy yêu cầu quyền như trong đoạn mã này:

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

[...]

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

Nếu bạn kiểm thử ứng dụng trên một thiết bị thực, dữ liệu sẽ bắt đầu cập nhật.

Kể từ Wear OS 4, trình mô phỏng cũng tự động hiển thị dữ liệu kiểm thử. Trên các phiên bản trước, bạn có thể mô phỏng luồng dữ liệu từ cảm biến. Trong cửa sổ dòng lệnh, hãy chạy lệnh ADB sau:

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

Để xem các giá trị nhịp tim khác nhau, hãy thử mô phỏng các bài tập khác nhau. Lệnh này mô phỏng hoạt động đi bộ:

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

Lệnh này mô phỏng hoạt động chạy:

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

Để dừng mô phỏng dữ liệu, hãy chạy lệnh sau:

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

Đọc dữ liệu về nhịp tim

Khi được cấp quyền BODY_SENSORS, bạn có thể đọc nhịp tim (heartRateMeasureFlow()) của người dùng trong HealthServicesManager. Trong giao diện người dùng của ứng dụng Wear OS, giá trị nhịp tim hiện tại sẽ xuất hiện, được đo bằng cảm biến trên thiết bị đeo.

Trong ViewModel, hãy bắt đầu thu thập dữ liệu bằng đối tượng luồng nhịp tim, như minh hoạ trong đoạn mã sau:

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

Sử dụng một đối tượng có thể kết hợp tương tự như đối tượng sau để hiển thị dữ liệu trực tiếp trong giao diện người dùng của ứng dụng:

val heartRate by viewModel.hr

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

Gửi dữ liệu đến thiết bị cầm tay

Để gửi dữ liệu sức khoẻ và thể chất đến một thiết bị cầm tay, hãy sử dụng lớp DataClient trong Dịch vụ sức khoẻ. Đoạn mã sau đây cho biết cách gửi dữ liệu nhịp tim mà ứng dụng của bạn đã thu thập trước đó:

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

Nhận dữ liệu trên điện thoại

Để nhận dữ liệu trên điện thoại, hãy tạo một 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
                }
            }
        }
    }
}

Sau khi hoàn tất bước này, hãy lưu ý một số chi tiết thú vị:

  • Chú thích @AndroidEntryPoint cho phép chúng ta sử dụng Hilt trong lớp này
  • @Inject lateinit var heartRateMonitor: HeartRateMonitor sẽ thực sự chèn một phần phụ thuộc vào lớp này
  • Lớp này triển khai onDataChanged() và nhận một tập hợp các sự kiện mà bạn có thể phân tích cú pháp và sử dụng

Logic HeartRateMonitor sau đây cho phép bạn gửi các giá trị nhịp tim đã nhận được đến một phần khác trong cơ sở mã của ứng dụng:

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

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

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

Một bus dữ liệu nhận các sự kiện từ phương thức onDataChanged() và cung cấp các sự kiện đó cho trình quan sát dữ liệu bằng cách sử dụng SharedFlow.

Phần cuối cùng là nội dung khai báo của Service trong ứng dụng điện thoại 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>

Hiển thị dữ liệu theo thời gian thực trên thiết bị cầm tay

Trong phần ứng dụng chạy trên thiết bị cầm tay, hãy chèn HeartRateMonitor vào hàm khởi tạo của mô hình hiển thị. Đối tượng HeartRateMonitor này theo dõi dữ liệu nhịp tim và phát ra các bản cập nhật giao diện người dùng khi cần.