使用感應器管理工具測量行動裝置的步數

如要使用感應器管理工具在行動應用程式中填入步數資料,請參閱本指南。如要進一步瞭解如何設計及管理運動應用程式 UI,請參閱「建構基本健身應用程式」。

開始使用

如要開始從行動裝置測量基本步數計數器的步數,您需要在應用程式模組 build.gradle 檔案中新增依附元件。確認您使用的是最新版本的依附元件。 此外,如果想將應用程式支援範圍擴展至其他板型規格 (例如 Wear OS),請新增這些板型規格所需的依附元件。

以下列舉幾個 UI 依附元件的範例。如需完整清單,請參閱這份 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. getSystemService() 取得 SensorManager 物件。
  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")
}

建立前景服務

在基本健身應用程式中,您可能會使用按鈕接收使用者的開始和停止事件,以便追蹤步數。

請注意感應器最佳做法。 具體來說,步數計數器感應器應只在感應器監聽器註冊時計算步數。將感應器註冊作業與前景服務建立關聯後,只要需要感應器,系統就會註冊感應器,且即使應用程式不在前景,感應器仍可保持註冊狀態。

在前景服務的 onPause() 方法中,使用下列程式碼片段取消註冊感應器:

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

分析活動資料

如要存取感應器資料,請實作 SensorEventListener 介面。請注意,您應將感應器註冊作業與前景服務的生命週期建立關聯,並在服務暫停或結束時取消註冊感應器。下列程式碼片段說明如何為 Sensor.TYPE_STEP_COUNTER 實作 SensorEventListener 介面:

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")
    }
}

建立感應器事件的資料庫

應用程式可能會顯示畫面,讓使用者查看一段時間內的步數。如要在應用程式中提供這項功能,請使用 Room 持續性資料庫

以下程式碼片段會建立資料表,其中包含一組步數測量值,以及應用程式存取每項測量值的時間:

@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 是負責執行背景作業的元件,可確保執行作業的可靠性。詳情請參閱 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. 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" />