How To Make an Android Run Tracking App

Learn how to make an Android run tracking app to show a user’s position on a map, along with their path. By Roberto Orgiu.

5 (1) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Showing User Movement

Now, you can track your user’s movement and mark their location on the map, but you really want to draw the path they moved across. To do so, you need to change the LocationProvider class.

Open LocationsProvider.kt and right below the locations list, add the distance:

private var distance = 0

Now your code updates the distance every time the software records a new position for the user. But, you need additional LiveData to emit the distance the user moves.

Below the declaration of liveLocation, add:

val liveLocations = MutableLiveData<List<LatLng>>()
val liveDistance = MutableLiveData<Int>()

Since the position update API needs a callback to work, you need to create the callback before you can ask for the location. Create this as a nested class inside LocationProvider.kt:

private val locationCallback = object : LocationCallback() {
    override fun onLocationResult(result: LocationResult) {
      //1
      val currentLocation = result.lastLocation
      val latLng = LatLng(currentLocation.latitude, currentLocation.longitude)

      //2
      val lastLocation = locations.lastOrNull()

      //3
      if (lastLocation != null) {
        distance += 
          SphericalUtil.computeDistanceBetween(lastLocation, latLng).roundToInt()
        liveDistance.value = distance
      }

      //4
      locations.add(latLng)
      liveLocations.value = locations
  }
}

Here’s a code breakdown:

  1. You get the currently recorded location, and tranform it into a LatLng that the map can plot easily.
  2. Then, you check if there are other locations. Since you need it to calculate the distance between the user’s last point and current point, you need the last location before the current one.
  3. If the current location is not the first one, you use the SphericalUtil functions to compute the distance between the two points and add it to the distance you emit through its LiveData.
  4. In the last instance, you add the current location to the list of recorded positions and emit it.

Now, the callback is ready, but you need to register it! Paste trackUser in LocationProvider:

fun trackUser() {
  //1
  val locationRequest = LocationRequest.create()
  locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
  locationRequest.interval = 5000

  //2
  client.requestLocationUpdates(locationRequest, locationCallback, 
        Looper.getMainLooper())
}

In the first part of the snippet, you create a request. This request should be at high accuracy since you want to track the user moving at a relatively slow speed. For the same reason, you set the recording’s interval to five seconds, which is more than enough for this kind of movement.

Finally, remove this callback when the user stops the tracking:

fun stopTracking() {
  client.removeLocationUpdates(locationCallback)
  locations.clear()
  distance = 0
}

This method clears all the data you’ve gathered so far, leaving the class ready to track again immediately!

You’re almost ready to draw that path. Time to do it!

Presenting Location Data

Sharing three LiveDatas in the UI, plus one for the step count, is a bit cumbersome. Instead, create something that can represent your screen’s state.

Inside your app’s main package, create a file named MVP.kt.

Note: For this tutorial, you’ll use a slimmed-down architecture that better fits the example and makes it easier to deal with this topic.

Inside your new file, paste this data class:

data class Ui(
    val formattedPace: String,
    val formattedDistance: String,
    val currentLocation: LatLng?,
    val userPath: List<LatLng>
) {

  companion object {

    val EMPTY = Ui(
        formattedPace = "",
        formattedDistance = "",
        currentLocation = null,
        userPath = emptyList()
    )
  }
}

This simple class contains all the information you need to render your UI correctly: The distance the user walks, their current location and the list of locations the app recorded. Great!

Now, you need something to get all the data from the sensors, glue them together and let the UI know that something new is available. In the same file, create a new class like this:

class MapPresenter(private val activity: AppCompatActivity) {
}

You’re closer to the win! Declare a field in the presenter that will emit the new data every time there’s an update:

val ui = MutableLiveData(Ui.EMPTY)

Next, create the dependencies you need, like the LocationProvider and the PermissionManager:

private val locationProvider = LocationProvider(activity)
private val permissionsManager = PermissionsManager(activity, locationProvider)

Now, you need to create a function that will glue all the data together inside the Ui data class. Add this function to the MapPresenter:

fun onViewCreated() {

}

At this point, you need to get the three LiveDatas from the LocationProvider, attaching an observer to each of them. For this task, you’ll need the Activity you have in the constructor, and to use the copy function of the data class.

Inside onViewCreated, paste:

locationProvider.liveLocations.observe(activity) { locations ->
  val current = ui.value
  ui.value = current?.copy(userPath = locations)
}

locationProvider.liveLocation.observe(activity) { currentLocation ->
  val current = ui.value
  ui.value = current?.copy(currentLocation = currentLocation)
}

locationProvider.liveDistance.observe(activity) { distance ->
  val current = ui.value
  val formattedDistance = activity.getString(R.string.distance_value, distance)
  ui.value = current?.copy(formattedDistance = formattedDistance)
}

For each LiveData you listen to, you get its value, optionally format it to something more readable and update the ui LiveData to emit the new data.

Now your IDE will complain that R.string.distance_value isn’t available: Time to fix that!

Open strings.xml and paste this line below the other string declarations:

<string name="distance_value">%d meters</string>

This label will take a number as parameter and place it where the %d is, so that if you pass 5, you get back a string saying 5 meters.

At this point, you want to add the logic to ask permission from the presenter by adding onMapLoaded:

fun onMapLoaded() {
  permissionsManager.requestUserLocation()
}

This method will run as soon as the Google Maps container is ready, just like earlier!

Now, add two methods to handle the user pressing the start and stop button in the UI. For the moment they only link to the LocationProvider:

fun startTracking() {
  locationProvider.trackUser()
}

fun stopTracking() {
  locationProvider.stopTracking()
}

This way, you only interact with one object, which is much easier and cleaner than dealing with many objects at once.

It’s time to build! Run your app and press the start button. Move around, and your app will track you and display your updated position.

android run tracking app with location

You’re getting closer!

Recognizing the User’s Activity

So far, you still lack the pace, or the number of steps the user takes, before you update your UI fully. This requires you to recognize what the user is doing.

Create another Kotlin file and name it StepCounter.kt. Inside this file, you’ll check the user’s activity and how many steps they take.

First, create a class that extends SensorEventListener:

class StepCounter(private val activity: AppCompatActivity) : SensorEventListener {

}

At the top of this class, you need to declare the LiveData containing the steps, an instance of the SensorManager that provides access to the specific sensors, and an instance of the as fields. Moreover, you’ll declare another variable that contains the initial steps because the number of steps resets at every boot. So at any given moment, you’ll get the number of steps the user walked from the last time they rebooted the phone.

With this variable, you’ll know exactly when you start observing, and you’ll give a much more precise measure of the steps the user took during the activity.

Insert the following code at the top of StepCounter:

val liveSteps = MutableLiveData<Int>()

private val sensorManager by lazy {
  activity.getSystemService(SENSOR_SERVICE) as SensorManager
}

private val stepCounterSensor: Sensor? by lazy {
  sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
}

private var initialSteps = -1

Since you implemented the SensorEventListener interface, you also need to implement a couple of methods. The first is the one you really care about. In the onSensorChanged function, you’ll get the updates you need.

You only need the second method, onAccuracyChanged, when you perform logic based on the accuracy. But it’s not your case, so it can simply return Unit:

override fun onSensorChanged(event: SensorEvent) {
  event.values.firstOrNull()?.toInt()?.let { newSteps ->
    //1
    if (initialSteps == -1) {
      initialSteps = newSteps
    }

    //2
    val currentSteps = newSteps - initialSteps

    //3
    liveSteps.value = currentSteps
  }
}

override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) = Unit

Pause a moment and analyze the first method. As you already know, the number of steps you receive is the one calculated from the device boot. So, to avoid confusion, you keep track of the steps by using the initialSteps variable.

  1. If this variable is still negative, it means you didn’t record the start yet, so you set it equal to the sensor data.
  2. Then, you calculate the delta between the new data from the sensor and the initial steps. The first time you get data, this difference be zero, as expected.
  3. In this last line, you set the LiveData to the currentSteps you calculated, so all the listeners will react to its changes.

Now, you only need two more methods: One adds this same class as listener to the SensorManager, while the other removes the listener:

fun setupStepCounter() {
  if (stepCounterSensor != null) {
    sensorManager.registerListener(this, stepCounterSensor, SENSOR_DELAY_FASTEST)
  }
}

fun unloadStepCounter() {
  if (stepCounterSensor != null) {
    sensorManager.unregisterListener(this)
  }
} 

Build your app to make sure that everything is in place. You might find that running your app causes it to crash: This is expected. After all, you still need some permissions to be ready!