איך להשתמש במנהל החיישנים כדי למדוד שלבים ממכשיר נייד

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

תחילת העבודה

כדי להתחיל למדוד את השלבים של מונה הצעדים הבסיסי בנייד, צריך להוסיף את יחסי התלות למודול האפליקציה קובץ build.gradle. חשוב להשתמש בגרסאות העדכניות של יחסי התלות. בנוסף, כשמרחיבים את התמיכה של האפליקציה לגורמי צורה אחרים, כמו Wear OS, מוסיפים את יחסי התלות שנדרשים לגורמי הצורה האלה.

בהמשך מוצגות כמה דוגמאות ליחסי התלות של ממשק המשתמש. לרשימה מלאה, לעיין במדריך הזה בנושא רכיבי UI.

implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.activity:activity-compose")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")

משיגים את החיישן של מונה הצעדים

אחרי שהמשתמש העניק את ההרשאה הנדרשת לזיהוי פעילות, ניתן לגשת לחיישן מונה הצעדים:

  1. משיגים את האובייקט SensorManager מ-getSystemService().
  2. צריך להשיג את החיישן של מונה הצעדים מה-SensorManager:
private val sensorManager by lazy {
        getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
        sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }

בחלק מהמכשירים אין חיישן מונה צעדים. עליך לבדוק את החיישן ותוצג הודעת שגיאה אם אין למכשיר את התג:

if (sensor == null) {
    Text(text = "Step counter sensor is not present on this device")
}

יצירת שירות שפועל בחזית

באפליקציות כושר בסיסיות עשויות לכלול לחצן כדי לקבל מהמשתמש אירועי התחלה ועצירה לגבי שלבי המעקב.

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

צריך להשתמש בקטע הקוד הבא כדי לבטל את רישום החיישן בשיטה onPause() של השירות שפועל בחזית:

override fun onPause() {
    super.onPause()
    sensorManager.unregisterListener(this)
}

ניתוח נתונים של אירועים

כדי לגשת לנתוני החיישנים, צריך להטמיע את הממשק של SensorEventListener. הערה שצריך לשייך את רישום החיישן לשירות שפועל בחזית במחזור החיים, ביטול רישום החיישן כשהשירות מושהה או מסתיים. קטע הקוד הבא מראה איך להטמיע את הממשק של SensorEventListener עבור Sensor.TYPE_STEP_COUNTER:

private const val TAG = "STEP_COUNT_LISTENER"

context(Context)
class StepCounter {
    private val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val sensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)

    suspend fun steps() = suspendCancellableCoroutine { continuation ->
        Log.d(TAG, "Registering sensor listener... ")

        val listener: SensorEventListener by lazy {
            object : SensorEventListener {
                override fun onSensorChanged(event: SensorEvent?) {
                    if (event == null) return

                    val stepsSinceLastReboot = event.values[0].toLong()
                    Log.d(TAG, "Steps since last reboot: $stepsSinceLastReboot")

                    if (continuation.isActive) {
                        continuation.resume(stepsSinceLastReboot)
                    }
                }

                override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
                      Log.d(TAG, "Accuracy changed to: $accuracy")
                }
            }
       }

        val supportedAndEnabled = sensorManager.registerListener(listener,
                sensor, SensorManager.SENSOR_DELAY_UI)
        Log.d(TAG, "Sensor listener registered: $supportedAndEnabled")
    }
}

יצירת מסד נתונים לאירועים מהחיישנים

ייתכן שבאפליקציה יוצג מסך שבו המשתמש יכול לראות את השלבים לאורך זמן. כדי לספק את היכולת הזו באפליקציה, צריך להשתמש בספרייה הקבועה של חדרים.

קטע הקוד הבא יוצר טבלה שמכילה קבוצה של ספירת צעדים המדידות, וגם הזמן שבו האפליקציה ניגשה לכל מדידה:

@Entity(tableName = "steps")
data class StepCount(
  @ColumnInfo(name = "steps") val steps: Long,
  @ColumnInfo(name = "created_at") val createdAt: String,
)

יצירה של אובייקט גישה לנתונים (DAO) כדי לקרוא ולכתוב את הנתונים:

@Dao
interface StepsDao {
    @Query("SELECT * FROM steps")
    suspend fun getAll(): List<StepCount>

    @Query("SELECT * FROM steps WHERE created_at >= date(:startDateTime) " +
            "AND created_at < date(:startDateTime, '+1 day')")
    suspend fun loadAllStepsFromToday(startDateTime: String): Array<StepCount>

    @Insert
    suspend fun insertAll(vararg steps: StepCount)

    @Delete
    suspend fun delete(steps: StepCount)
}

כדי ליצור אובייקט DAO, יוצרים אובייקט RoomDatabase:

@Database(entities = [StepCount::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun stepsDao(): StepsDao
}

אחסון נתוני החיישנים במסד הנתונים

ב-ViewModel נעשה שימוש במחלקה StepCounter החדשה, כך שאפשר לאחסן את השלבים בהקדם תוך כדי קריאה:

viewModelScope.launch {
    val stepsFromLastBoot = stepCounter.steps()
    repository.storeSteps(stepsFromLastBoot)
}

הכיתה repository תיראה כך:

class Repository(
    private val stepsDao: StepsDao,
) {

    suspend fun storeSteps(stepsSinceLastReboot: Long) = withContext(Dispatchers.IO) {
        val stepCount = StepCount(
            steps = stepsSinceLastReboot,
            createdAt = Instant.now().toString()
        )
        Log.d(TAG, "Storing steps: $stepCount")
        stepsDao.insertAll(stepCount)
    }

    suspend fun loadTodaySteps(): Long = withContext(Dispatchers.IO) {
        printTheWholeStepsTable() // DEBUG

        val todayAtMidnight = (LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT).toString())
        val todayDataPoints = stepsDao.loadAllStepsFromToday(startDateTime = todayAtMidnight)
        when {
            todayDataPoints.isEmpty() -> 0
            else -> {
                val firstDataPointOfTheDay = todayDataPoints.first()
                val latestDataPointSoFar = todayDataPoints.last()

                val todaySteps = latestDataPointSoFar.steps - firstDataPointOfTheDay.steps
                Log.d(TAG, "Today Steps: $todaySteps")
                todaySteps
            }
        }
    }
}

אחזור נתוני חיישנים מדי פעם

אם משתמשים בשירות שפועל בחזית, אין צורך להגדיר את WorkManager כי במהלך הזמן שבו האפליקציה עוקבת באופן פעיל אחר הצעדים של המשתמש, מספר הצעדים הכולל המעודכן אמור להופיע באפליקציה.

עם זאת, אם ברצונך לקבץ את רשומות השלבים, אפשר להשתמש ב-WorkManager כדי למדוד צעדים במרווח זמן ספציפי, כמו פעם בכל 15 דקות. WorkManager הוא הרכיב שמבצע את הרקע לביצוע מובטח. מידע נוסף זמין בקוד Lab של WorkManager.

כדי להגדיר את האובייקט Worker לאחזור הנתונים, צריך לשנות את doWork() , כפי שמוצג בקטע הקוד הבא:

private const val TAG = " StepCounterWorker"

@HiltWorker
class StepCounterWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted workerParams: WorkerParameters,
    val repository: Repository,
    val stepCounter: StepCounter
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        Log.d(TAG, "Starting worker...")

        val stepsSinceLastReboot = stepCounter.steps().first()
        if (stepsSinceLastReboot == 0L) return Result.success()

        Log.d(TAG, "Received steps from step sensor: $stepsSinceLastReboot")
        repository.storeSteps(stepsSinceLastReboot)

        Log.d(TAG, "Stopping worker...")
        return Result.success()
    }
}

כדי להגדיר את WorkManager כך שיאחסן את ספירת הצעדים הנוכחית כל 15 דקות, צריך לבצע הבאים:

  1. צריך להרחיב את המחלקה Application כדי להטמיע את Configuration.Provider גרפי.
  2. באמצעות method onCreate(), מוסיפים PeriodicWorkRequestBuilder לתור.

התהליך הזה מופיע בקטע הקוד הבא:

@HiltAndroidApp
@RequiresApi(Build.VERSION_CODES.S)
internal class PulseApplication : Application(), Configuration.Provider {

    @Inject
    lateinit var workerFactory: HiltWorkerFactory

    override fun onCreate() {
        super.onCreate()

        val myWork = PeriodicWorkRequestBuilder<StepCounterWorker>(
                15, TimeUnit.MINUTES).build()

        WorkManager.getInstance(this)
            .enqueueUniquePeriodicWork("MyUniqueWorkName",
                    ExistingPeriodicWorkPolicy.UPDATE, myWork)
    }

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(workerFactory)
            .setMinimumLoggingLevel(android.util.Log.DEBUG)
            .build()
}

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

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />