כדי לשפר את חוויית השימוש באפליקציה בתחום הבריאות והכושר, אפשר להרחיב אותה למכשירים לבישים עם Wear OS.
הוספת מודול Wear OS
Android Studio מספק אשף שימושי להוספת מודול Wear OS לאפליקציה. בתפריט File > New Module (קובץ > מודול חדש), בוחרים באפשרות Wear OS, כמו שמוצג בתמונה הבאה:
חשוב לציין שגרסת המינימום של SDK צריכה להיות API 30 ומעלה כדי שתוכלו להשתמש בגרסה העדכנית ביותר של שירותי הבריאות. שירותי בריאות מאפשרים לעקוב אחרי מדדים ולתעד נתונים בקלות על ידי הגדרה אוטומטית של חיישני בריאות.
אחרי שמסיימים את השימוש באשף, מסנכרנים את הפרויקט. מופיעה ההגדרה הבאה של Run:
כך תוכלו להריץ את מודול 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
הזה עוקב אחרי נתוני הדופק ומעדכן את ממשק המשתמש לפי הצורך.