Usa el Administrador de sensores para propagar los datos de los pasos en una app para dispositivos móviles, como se describe en esta guía. Si quieres obtener más información para diseñar y administrar la IU de una app de ejercicios, consulta Cómo compilar una app de fitness básica.
Cómo empezar
Para comenzar a medir los pasos del contador de pasos básico desde tu dispositivo móvil, deberás agregar las dependencias al archivo build.gradle
del módulo de tu app. Asegúrate de usar las versiones más recientes de las dependencias.
Además, cuando extiendas la compatibilidad de tu app a otros factores de forma, como Wear OS, agrega las dependencias que requieren estos factores.
A continuación, se muestran algunos ejemplos de algunas de las dependencias de la IU. Para obtener una lista completa, consulta esta guía sobre elementos de la IU.
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.activity:activity-compose")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")
Obtén el sensor del contador de pasos
Después de que el usuario otorgue el permiso de reconocimiento de actividad necesario, podrás acceder al sensor del contador de pasos:
- Obtén el objeto
SensorManager
degetSystemService()
. - Adquiere el sensor del contador de pasos en
SensorManager
:
private val sensorManager by lazy {
getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }
Algunos dispositivos no tienen el sensor del contador de pasos. Debes buscar el sensor y mostrar un mensaje de error si el dispositivo no tiene uno:
if (sensor == null) {
Text(text = "Step counter sensor is not present on this device")
}
Cómo crear tu servicio en primer plano
En una app básica de fitness, es posible que tengas un botón para recibir eventos de inicio y detención del usuario y, así, hacer un seguimiento de los pasos.
Ten en cuenta las prácticas recomendadas relacionadas con los sensores. En particular, el sensor del contador de pasos solo debe contar pasos mientras el objeto de escucha del sensor está registrado. Cuando se asocia el registro del sensor con un servicio en primer plano, el sensor se registra siempre que sea necesario y puede permanecer registrado cuando la app no está en primer plano.
Usa el siguiente fragmento para cancelar el registro del sensor en el método onPause()
de tu servicio en primer plano:
override fun onPause() {
super.onPause()
sensorManager.unregisterListener(this)
}
Analizar datos en busca de eventos
Para acceder a los datos del sensor, implementa la interfaz SensorEventListener
. Ten en cuenta que debes asociar el registro del sensor con el ciclo de vida de tu servicio en primer plano y cancelar el registro del sensor cuando el servicio se pone en pausa o finaliza. En el siguiente fragmento, se muestra cómo implementar la interfaz SensorEventListener
para 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")
}
}
Crea una base de datos para los eventos de sensores
Tu app puede mostrar una pantalla en la que el usuario pueda ver sus pasos a lo largo del tiempo. Para proporcionar esta función en tu app, usa la biblioteca de persistencias Room.
En el siguiente fragmento, se crea una tabla que contiene un conjunto de mediciones del recuento de pasos, junto con la hora en la que la app accedió a cada medición:
@Entity(tableName = "steps")
data class StepCount(
@ColumnInfo(name = "steps") val steps: Long,
@ColumnInfo(name = "created_at") val createdAt: String,
)
Crea un objeto de acceso de datos (DAO) para leer y escribir los datos:
@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)
}
Para crear una instancia del DAO, crea un objeto RoomDatabase
:
@Database(entities = [StepCount::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun stepsDao(): StepsDao
}
Almacena los datos del sensor en la base de datos
ViewModel usa la nueva clase StepCounter, por lo que puedes almacenar los pasos en cuanto los leas:
viewModelScope.launch {
val stepsFromLastBoot = stepCounter.steps()
repository.storeSteps(stepsFromLastBoot)
}
La clase repository
se verá de la siguiente manera:
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
}
}
}
}
Cómo recuperar datos del sensor de forma periódica
Si usas un servicio en primer plano, no necesitas configurar WorkManager
porque, durante el tiempo en que la app realiza un seguimiento activo de los pasos del usuario, el recuento total actualizado debería aparecer en la app.
Sin embargo, si deseas agrupar los registros de pasos por lotes, puedes usar WorkManager
para medir los pasos en un intervalo específico, por ejemplo, una vez cada 15 minutos.
WorkManager
es el componente que realiza el trabajo en segundo plano para una ejecución garantizada. Obtén más información en el codelab de WorkManager.
Si deseas configurar el objeto Worker
para recuperar los datos, anula el método doWork()
, como se muestra en el siguiente fragmento de código:
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()
}
}
Si deseas configurar WorkManager
para que almacene el recuento de pasos actual cada 15 minutos, haz lo siguiente:
- Extiende la clase
Application
para implementar la interfazConfiguration.Provider
. - En el método
onCreate()
, coloca unPeriodicWorkRequestBuilder
en la cola.
Este proceso aparece en el siguiente fragmento de código:
@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()
}
Para inicializar el proveedor de contenido que controla el acceso a la base de datos del contador de pasos de tu app inmediatamente después de que se inicia la app, agrega el siguiente elemento al archivo de manifiesto de la app:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />