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

Cải thiện trải nghiệm về sức khoẻ và thể chất của ứng dụng bằng cách mở rộng ứng dụng trên các thiết bị đeo sử dụng Wear OS.

Thêm mô-đun Wear OS

Android Studio cung cấp một trình hướng dẫn tiện lợi để 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ư trong hình sau:

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

Điều quan trọng bạn cần lưu ý là Minimum SDK (SDK tối thiểu) phải là API 30 trở lên thì mới cho phép bạn sử dụng phiên bản mới nhất của Dịch vụ sức khoẻ. 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ảm biến sức khoẻ.

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

Hình ảnh cho thấy 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:

Việc chạy cấu hình này sẽ triển khai ứng dụng cho trình mô phỏng hoặc thiết bị Wear OS và hiển thị trải nghiệm "xin chào thế giới". Đây là cá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 tạo ứng dụng.

Thêm Dịch vụ sức khoẻ và Hilt

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

  • Dịch vụ sức khoẻ: giúp việc truy cập vào các cảm biến và dữ liệu trên đồng hồ rất thuận tiện và tiết kiệm điện năng 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ẻ

Để việc sử dụng Dịch vụ sức khoẻ trở nên thuận tiện hơn cũng như hiển thị 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 tạo mô-đun Hilt để quản lý mô-đun, bạn có thể sử 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() giúp đăng ký trình nghe thiết bị theo dõi nhịp tim và phát dữ liệu đã nhận.

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 như vậy, 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 cấp quyền như minh hoạ trong đoạn mã sau:

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, thì dữ liệu sẽ bắt đầu cập nhật.

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

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 quá trình 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 nhịp tim

Khi được cấp quyền BODY_SENSORS, bạn có thể đọc nhịp tim của người dùng (heartRateMeasureFlow()) 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 do cảm biến trên thiết bị đeo đo.

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ư 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 một thiết bị cầm tay

Để gửi dữ liệu sức khoẻ và hoạt động thể dục đến một thiết bị cầm tay, hãy 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 đây:

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 thành bước này, hãy chú ý 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 thực sự 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 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)
    }
}

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 đối tượng tiếp nhận dữ liệu bằng cách sử dụng SharedFlow.

Bit cuối cùng là phần khai báo 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 một thiết bị cầm tay

Trong một phần của ứ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 quan sát dữ liệu nhịp tim và phát ra thông tin cập nhật trên giao diện người dùng khi cần.