دمج وحدة Wear OS

يمكنك تحسين تجربة الصحة واللياقة البدنية على تطبيقك من خلال توفيره على الأجهزة القابلة للارتداء التي تعمل بنظام التشغيل Wear OS.

إضافة وحدة نمطية لنظام التشغيل Wear OS

يوفّر "استوديو Android" معالجًا مفيدًا لإضافة وحدة Wear OS إلى تطبيقك. في القائمة ملف > وحدة جديدة، اختَر Wear OS، كما هو موضّح في الصورة التالية:

معالج وحدة Wear OS في "استوديو Android"
الشكل 1: إنشاء وحدة Wear OS

تجدر الإشارة إلى أنّ الحدّ الأدنى لحزمة تطوير البرامج (SDK) يجب أن يكون واجهة برمجة التطبيقات 30 أو إصدار أحدث للسماح لك باستخدام أحدث إصدار من "خدمات الصحة". تسهّل الخدمات الصحية تتبُّع المقاييس وتسجيل البيانات من خلال ضبط أدوات استشعار السلامة تلقائيًا.

بعد إكمال المعالج، قم بمزامنة مشروعك. تظهر إعدادات Run التالية:

صورة تعرض زر التشغيل في تطبيق Wear OS
الشكل 2: زر التشغيل لوحدة Wear OS الجديدة

يتيح لك هذا الإجراء تشغيل وحدة Wear OS على جهاز قابل للارتداء. اختيار أيّ من الإجراءين أدناه:

يؤدي تفعيل الإعدادات إلى نشر التطبيق على محاكي أو جهاز Wear OS وعرض تجربة "مرحبًا بالعالم". هذا هو الإعداد الأساسي لواجهة المستخدم التي تستخدم Compose for Wear OS للبدء في استخدام تطبيقك.

إضافة خدمات صحية وHilt

يمكنك دمج المكتبات التالية في وحدة Wear OS:

  • الخدمات الصحية: تجعل الوصول إلى أجهزة الاستشعار والبيانات على الساعة مريحة للغاية وأكثر فعالية من حيث الطاقة.
  • Hilt: يسمح بتطبيق التبعية وإدارتها بشكل فعال.

إنشاء مدير الخدمات الصحية

لتسهيل استخدام Health Services، وعرض واجهة برمجة تطبيقات أصغر وأكثر سلاسة، يمكنك إنشاء برنامج تضمين على النحو التالي:

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"، يمكنك قراءة معدّل نبضات قلب المستخدم (heartRateMeasureFlow()) في "HealthServicesManager". في واجهة المستخدم في تطبيق Wear OS، تظهر القيمة الحالية لمعدّل نبضات القلب بعد قياسها بواسطة أداة الاستشعار على الجهاز القابل للارتداء.

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

يمكنك استخدام عنصر قابل للإنشاء مشابه لما يلي لعرض البيانات المباشرة في واجهة المستخدم الخاصة بتطبيقك:

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.

البت الأخير هو تعريف Service في تطبيق الهاتف 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>

عرض البيانات في الوقت الفعلي على جهاز محمول

في جزء التطبيق الذي يتم تشغيله على جهاز محمول، أدخِل HeartRateMonitor في الدالة الإنشائية لنموذج العرض. يلاحظ عنصر HeartRateMonitor هذا بيانات معدّل نبضات القلب ويصدر تحديثات في واجهة المستخدم حسب الحاجة.