הטמעה של מודול Wear OS

יש לך אפשרות לשפר את חוויית הבריאות והכושר של האפליקציה באמצעות הרחבה שלה לגאדג'טים לבישים מכשירים שמופעלים על ידי Wear OS.

הוספת מודול של Wear OS

ב-Android Studio יש אשף שימושי להוספת מודול Wear OS לאפליקציה. לחשבון את File > בתפריט 'מודול חדש', בוחרים באפשרות Wear OS, כמו שמוצג בהמשך תמונה:

אשף המודול של Wear OS ב-Android Studio
איור 1: יוצרים מודול של Wear OS

חשוב לציין שערך ה-SDK המינימלי חייב להיות מבוסס על API מגרסה 30 ואילך כדי לאפשר לך להשתמש בגרסה העדכנית ביותר של שירותי הבריאות. שירותי בריאות מאפשר לעקוב בקלות אחרי מדדים ולתעד נתונים על ידי הגדרת תקינות חיישנים אוטומטיים.

לאחר השלמת האשף, מסנכרנים את הפרויקט. ההרצה הבאה: מופיעה:

תמונה שמוצג בה לחצן הריצה של האפליקציה ל-Wear OS
איור 2: לחצן ההפעלה למודול Wear OS החדש

ההרשאה הזו מאפשרת להריץ את המודול של Wear OS במכשיר לביש. עומדות לרשותך שתי אפשרויות:

כשמפעילים את ההגדרות האישיות, האפליקציה פורסת את האמולטור של Wear OS או ומראה 'שלום עולם' חוויה אישית. זו ההגדרה הבסיסית של ממשק המשתמש, באמצעות כתיבת הודעה ל-Wear OS כדי להתחיל לעבוד עם האפליקציה.

הוספת שירותי בריאות והיטל

משלבים את הספריות הבאות במודול Wear OS:

  • Health Services: מאפשרת גישה לחיישנים ולנתונים בשעון נוח מאוד וחסכוני יותר בחשמל.
  • Hilt: מאפשר החדרה וניהול יעילים של תלות.

יצירת חשבון של מנהל שירותי הבריאות

כדי להפוך את השימוש בשירותי בריאות לנוח יותר, וכדי לחשוף ו-smoother API, אפשר ליצור wrapper באופן הבא:

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 מספקת את ה-method 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 הבאה מאפשרת לשלוח את קצב הלב שהתקבל לחלק אחר ב-codebase של האפליקציה:

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 ב-constructor של המודל של התצוגה המפורטת. הHeartRateMonitor הזה האובייקט בודק את נתוני הדופק ופולט עדכונים בממשק המשתמש לפי הצורך.