Use o Gerenciador de sensores para preencher dados de etapas em um app para dispositivos móveis, conforme descrito neste guia. Para mais informações sobre como projetar e gerenciar a interface de um app de exercícios, consulte Criar um app básico de condicionamento físico.
Como começar
Para começar a medir as etapas do seu contador de passos básico no seu
dispositivo móvel, adicione as dependências ao arquivo
build.gradle
do módulo do app. Verifique se você está usando as versões mais recentes das dependências.
Além disso, ao estender o suporte do app a outros formatos, como o Wear OS,
adicione as dependências exigidas por esses formatos.
Confira abaixo alguns exemplos de algumas das dependências da interface. Para conferir uma lista completa, consulte este guia de Elementos da interface.
implementation(platform("androidx.compose:compose-bom:2023.10.01"))
implementation("androidx.activity:activity-compose")
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.material:material")
Encontrar o sensor contador de passos
Depois que o usuário conceder a permissão de reconhecimento de atividades necessária, você vai poder acessar o sensor contador de passos:
- Extraia o objeto
SensorManager
dogetSystemService()
. - Adquira o sensor contador de passos do
SensorManager
:
private val sensorManager by lazy {
getSystemService(Context.SENSOR_SERVICE) as SensorManager }
private val sensor: Sensor? by lazy {
sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER) }
Alguns dispositivos não têm esse sensor. Verifique o sensor e mostre uma mensagem de erro se o dispositivo não tiver um:
if (sensor == null) {
Text(text = "Step counter sensor is not present on this device")
}
Criar o serviço em primeiro plano
Em um app fitness básico, você pode ter um botão para receber eventos de início e parada do usuário e monitorar passos.
Confira as práticas recomendadas sobre sensores. Especificamente, o sensor contador só vai contar passos enquanto o listener estiver registrado. Ao associar o registro do sensor a um serviço em primeiro plano, o sensor é registrado pelo tempo que for necessário, podendo permanecer registrado quando o app não está em primeiro plano.
Use o snippet a seguir para cancelar o registro do sensor no método onPause()
do
serviço em primeiro plano:
override fun onPause() {
super.onPause()
sensorManager.unregisterListener(this)
}
Analisar dados de eventos
Para acessar os dados do sensor, implemente a interface SensorEventListener
. Associe
o registro do sensor ao ciclo de vida do serviço em primeiro plano, cancelando o registro do sensor quando o serviço for pausado ou encerrado. O
snippet a seguir mostra como implementar a interface 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")
}
}
Criar um banco de dados para os eventos do sensor
O app pode mostrar uma tela em que o usuário pode conferir os passos ao longo do tempo. Para oferecer esse recurso no seu app, use a biblioteca de persistência do Room.
O snippet a seguir cria uma tabela que contém um conjunto de medidas de contagem de passos, além do horário em que seu app acessou cada medição:
@Entity(tableName = "steps")
data class StepCount(
@ColumnInfo(name = "steps") val steps: Long,
@ColumnInfo(name = "created_at") val createdAt: String,
)
Crie um objeto de acesso a dados (DAO, na sigla em inglês) para ler e gravar os dados:
@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 instanciar o DAO, crie um objeto RoomDatabase
:
@Database(entities = [StepCount::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun stepsDao(): StepsDao
}
Armazenar os dados do sensor no banco de dados
O ViewModel usa a nova classe StepCounter para que você possa armazenar as etapas assim que as ler:
viewModelScope.launch {
val stepsFromLastBoot = stepCounter.steps()
repository.storeSteps(stepsFromLastBoot)
}
A classe repository
ficaria assim:
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
}
}
}
}
Recuperar periodicamente os dados do sensor
Se você usa um serviço em primeiro plano, não precisa configurar WorkManager
porque, durante o período em que o app está monitorando ativamente os passos do usuário,
a contagem total de passos atualizada aparece no app.
No entanto, se você quiser agrupar os registros de passos, use WorkManager
para medir as etapas em um intervalo específico, como uma vez a cada 15 minutos.
WorkManager
é o componente que realiza o trabalho em segundo plano
para uma execução garantida. Saiba mais no codelab do WorkManager.
Para configurar o objeto Worker
para extrair os dados, substitua o método doWork()
, conforme mostrado no snippet de código a seguir.
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()
}
}
Para configurar o WorkManager
para armazenar a contagem de passos atual a cada 15 minutos, faça
o seguinte:
- Estenda a classe
Application
para implementar a interfaceConfiguration.Provider
. - No método
onCreate()
, coloque umPeriodicWorkRequestBuilder
na fila.
Esse processo aparece no snippet de código a seguir:
@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 o provedor de conteúdo que controla o acesso ao banco de dados do contador de passos do app imediatamente após a inicialização, adicione o seguinte elemento ao arquivo de manifesto do app:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />