Jetpack Compose: Step Tracker Integration

Integrating a step tracker into your Android app using Jetpack Compose can provide users with valuable health and fitness data. In this blog post, we’ll walk through the process of creating a step tracker integration with Jetpack Compose, utilizing the Android Sensors API and Accompanist Permissions library to handle permissions gracefully.

Understanding the Step Tracker Integration

A step tracker integration involves utilizing device sensors to count the steps a user takes throughout the day. It provides real-time feedback and historical data to help users monitor their physical activity. Building such a feature requires accessing sensor data, handling permissions, and updating the UI in real-time using Jetpack Compose.

Prerequisites

  • Android Studio with Jetpack Compose support
  • Basic knowledge of Kotlin and Jetpack Compose
  • Familiarity with Android Sensors API and permissions

Step-by-Step Guide

Step 1: Add Dependencies

First, add the necessary dependencies to your build.gradle file. We’ll use Accompanist for handling permissions and core-ktx for coroutines:

dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
    implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
    implementation("androidx.activity:activity-compose:1.7.0")
    implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    // Accompanist Permissions
    implementation("com.google.accompanist:accompanist-permissions:0.30.1")
}

Step 2: Create a Step Tracker Service

Create a service to manage sensor data and step counting:


import android.app.Service
import android.content.Context
import android.content.Intent
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.IBinder
import android.util.Log
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

class StepTrackerService : Service(), SensorEventListener {

    private var sensorManager: SensorManager? = null
    private var stepSensor: Sensor? = null

    private val _stepCount = MutableStateFlow(0)
    val stepCount = _stepCount.asStateFlow()

    override fun onCreate() {
        super.onCreate()
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        stepSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)

        if (stepSensor == null) {
            Log.e("StepTrackerService", "Step sensor not available on this device")
        } else {
            sensorManager?.registerListener(this, stepSensor, SensorManager.SENSOR_DELAY_NORMAL)
            Log.d("StepTrackerService", "Step sensor registered")
        }
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onSensorChanged(event: SensorEvent?) {
        if (event?.sensor?.type == Sensor.TYPE_STEP_COUNTER) {
            val steps = event.values[0].toInt()
            _stepCount.value = steps
            Log.d("StepTrackerService", "Step count: $steps")
        }
    }

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

    override fun onDestroy() {
        super.onDestroy()
        sensorManager?.unregisterListener(this)
        Log.d("StepTrackerService", "Step sensor unregistered")
    }
}

Key components of the StepTrackerService:

  • Sensor Management: Obtains the SensorManager and Sensor.TYPE_STEP_COUNTER to track steps.
  • Sensor Registration: Registers the service as a listener to the step counter sensor.
  • Step Counting: Updates a MutableStateFlow named stepCount whenever a step is detected.
  • Lifecycle Management: Ensures sensors are registered and unregistered during the service’s lifecycle.

Step 3: Requesting Sensor Permissions

Use Accompanist to request necessary permissions in your Composable. Update your AndroidManifest.xml to include the required permission:

Now, create a composable to request the necessary permission. The ‘ACTIVITY_RECOGNITION’ permission requires API >= 29:


import android.Manifest
import android.content.Intent
import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState

@Composable
fun StepTrackerPermissionRequest() {
    val context = LocalContext.current
    val permissionState = rememberPermissionState(
        permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            Manifest.permission.ACTIVITY_RECOGNITION
        } else {
            android.Manifest.permission.BODY_SENSORS
        }
    )

    LaunchedEffect(key1 = true) {
        if (!permissionState.status.isGranted) {
            permissionState.launchPermissionRequest()
        } else {
            // Permission already granted, start the service
            Intent(context, StepTrackerService::class.java).also { intent ->
                context.startService(intent)
            }
        }
    }
}

This composable checks for the permission, requests it if necessary, and starts the StepTrackerService when the permission is granted.

Step 4: Displaying Step Count in Compose UI

Create a Composable to display the step count and integrate it into your UI:


import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp

@Composable
fun StepCountDisplay() {
    val context = LocalContext.current
    val stepTrackerService = remember {
        StepTrackerService() //Use the same instance to not restart it everytime it recomposes
    }

    val stepCount by stepTrackerService.stepCount.collectAsState()

    Column(modifier = Modifier.padding(16.dp)) {
        Text(text = "Steps: $stepCount")
    }
}

Step 5: Integrate Everything in Your Activity

Finally, integrate the permission request and step count display in your main Activity:


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    StepTrackerPermissionRequest()
                    StepCountDisplay()
                }
            }
        }
    }
}

Best Practices and Considerations

  • Battery Optimization: Minimize battery usage by optimizing sensor event listening and avoiding unnecessary calculations.
  • Data Persistence: Persist step data to handle app restarts and provide historical tracking.
  • Error Handling: Handle cases where the sensor is not available or permissions are denied.
  • Background Services: Use foreground services with appropriate notifications for long-running background tasks like step tracking.
  • Testing: Thoroughly test the integration on various devices and Android versions to ensure reliability.

Conclusion

Integrating a step tracker into your Jetpack Compose app enhances its health and fitness capabilities, providing users with valuable insights into their physical activity. By handling sensor data and permissions efficiently, and following best practices, you can create a reliable and user-friendly step tracker integration. Using Jetpack Compose components alongside the Sensors API makes the development process more streamlined and maintainable.