Usa Sensor Manager para propagar datos de pasos en una app para dispositivos móviles, como se describe en este . Para obtener más información sobre cómo diseñar y administrar la IU de una app de ejercicios, consultar Compila una app de fitness básica.
Cómo comenzar
Para comenzar, mide los pasos del contador de pasos básico de tu
dispositivo móvil, deberás agregar las dependencias al módulo de tu app
build.gradle
. Asegúrate de usar las versiones más recientes de las dependencias.
Además, si extiendes la compatibilidad de tu app a otros factores de forma, como Wear OS,
agrega las dependencias que requieren estos factores de forma.
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, haz lo siguiente: puedes acceder al sensor del contador de pasos:
- Obtén el objeto
SensorManager
degetSystemService()
. - Adquiere el sensor del contador de pasos de
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 sensor del contador de pasos. Deberías revisar 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")
}
Crea tu servicio en primer plano
En una app de fitness básica, es posible que tengas un botón para recibir eventos de inicio y detención del usuario para realizar un seguimiento de los pasos.
Ten en cuenta las prácticas recomendadas para el sensor. En particular, el sensor del contador de pasos solo debe hacerlo mientras el sensor cuando se registra un objeto de escucha. Asociando el registro del sensor con un primer plano servicio, el sensor se registra durante el tiempo que sea necesario, y este se puede permanecerán registrados 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 para eventos
Para acceder a los datos del sensor, implementa la interfaz SensorEventListener
. Nota
que debes asociar el registro del sensor con el ID de tu servicio
ciclo de vida y cancelar el registro del sensor
cuando el servicio esté pausado o finalizado. El
El siguiente fragmento 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 podría 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.
Con el siguiente fragmento, se crea una tabla que contiene un conjunto de 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,
)
Crear 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
}
Almacenar los datos del sensor en la base de datos
ViewModel usa la nueva clase StepCounter para que puedas almacenar los pasos en cuanto a medida que los leas:
viewModelScope.launch {
val stepsFromLastBoot = stepCounter.steps()
repository.storeSteps(stepsFromLastBoot)
}
La clase repository
se vería 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
}
}
}
}
Recuperación periódica de los datos de sensores
Si usas un servicio en primer plano, no necesitas configurar WorkManager
ya que durante el tiempo en que la app
realiza un seguimiento activo de los pasos
el recuento total de pasos actualizado debería aparecer en tu app.
Sin embargo, si deseas agrupar los registros de pasos por lotes, puedes usar WorkManager
para
medir pasos en un intervalo específico, por ejemplo, una vez cada 15 minutos.
WorkManager
es el componente que realiza la ejecución en segundo plano.
trabajo para una ejecución garantizada. Obtén más información en el codelab de WorkManager.
Para configurar el objeto Worker
a fin de recuperar los datos, anula 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 quieres configurar WorkManager
para que almacene el recuento actual de pasos cada 15 minutos, sigue estos pasos:
lo siguiente:
- Extiende la clase
Application
para implementarConfiguration.Provider
interfaz de usuario. - En el método
onCreate()
, pon en cola unPeriodicWorkRequestBuilder
.
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 al paso de tu app, sigue estos pasos: contador de datos inmediatamente después de iniciar la app, agrega el siguiente elemento a el archivo de manifiesto de tu app:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />