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

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

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

‫Android Studio מספק אשף שימושי להוספת מודול Wear OS לאפליקציה. בתפריט File > New Module (קובץ > מודול חדש), בוחרים באפשרות Wear OS, כמו שמוצג בתמונה הבאה:

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

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

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

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

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

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

הוספה של Health Services ו-Hilt

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

  • שירותי בריאות: מאפשרים גישה נוחה יותר לחיישנים ולנתונים בשעון, וגם חוסכים בצריכת החשמל.
  • Hilt: מאפשר הזרקה וניהול יעילים של תלות.

יצירת Health Services Manager

כדי להשתמש ב-Health Services בצורה נוחה יותר, ולחשוף 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 החדש מספק שיטה 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 ב-Health Services. בקטע הקוד הבא מוצג איך לשלוח נתוני דופק שהאפליקציה אספה בעבר:

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