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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make an Android Run Tracking App
30 mins
- Getting Started
- Location, Location, Location
- Getting the User’s Location
- Asking Permission to Get Your User’s Location
- Testing for the First Time
- Showing User Movement
- Presenting Location Data
- Recognizing the User’s Activity
- Asking for Another Permission
- Adding More Glue
- Drawing the UI, At Last!
- Where to Go From Here?
Asking for Another Permission
Starting with Android Q, recognizing a user’s activity requires a new permission. So, you need to add more logic inside the PermissionManager
.
First, change the constructor of the PermissionManager
so that it accepts an instance of the StepCounter
:
class PermissionsManager(activity: AppCompatActivity,
private val locationProvider: LocationProvider,
private val stepCounter: StepCounter)
Then, declare the callback that runs when the user grants the permission on Android Q and newer. This will set up the step counter:
private val activityRecognitionPermissionProvider =
activity.registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (granted) {
stepCounter.setupStepCounter()
}
}
Finally, create a method that asks for the permission on Android Q and newer, or simply setup the step counter on older versions of the operating system:
fun requestActivityRecognition() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
activityRecognitionPermissionProvider.launch(Manifest.permission.ACTIVITY_RECOGNITION)
} else {
stepCounter.setupStepCounter()
}
}
Adding More Glue
Now, head back to MapPresenter.kt and add a new instance of the StepCounter
close to the declaration of the LocationProvider
:
private val stepCounter = StepCounter(activity)
You also need to pass this new stepCounter
to the newly updated PermissionsManager:
private val permissionsManager = PermissionsManager(activity, locationProvider, stepCounter)
Now, scroll to onViewCreated
and add a listener to the LiveData
, so that the data from the StepCounter
can flow with the others you previously listened to:
stepCounter.liveSteps.observe(activity) { steps ->
val current = ui.value
ui.value = current?.copy(formattedPace = "$steps")
}
Next, add the permission request for the number of steps in startTracking
. Since the user can remove permissions at any moment, every time they track their activities, you’ll need to ask for the permission again.
Don’t worry. If you have the permission already, the callback will run automatically:
permissionsManager.requestActivityRecognition()
Finally, unload the StepCounter
when the user stops recording their activity, in onStopTracking
:
stepCounter.unloadStepCounter()
The only thing left to take care of is the UI. Time to do it!
Drawing the UI, At Last!
Open MapsActivity.kt. You don’t need any reference to the PermissionManager
nor to the LocationProvider
, so, delete them. In their place, add a reference to the MapPresenter
:
private val presenter = MapPresenter(this)
Once you do this, in your activity’s onCreate
, use the binding
to reach out for the start button and add the following logic:
binding.btnStartStop.setOnClickListener {
if (binding.btnStartStop.text == getString(R.string.start_label)) {
//1
startTracking()
binding.btnStartStop.setText(R.string.stop_label)
} else {
//2
stopTracking()
binding.btnStartStop.setText(R.string.start_label)
}
}
In this snippet, first, you check if the button’s label is START
. If it is, you start the tracking and change the label to STOP
. Otherwise, you assume the label is STOP
already, stop the tracking and change the label back to START
.
The IDE is complaining, isn’t it? You’ll deal with it in a second. But first, add the presenter callback right below the bindings for the button:
presenter.onViewCreated()
This method tells the presenter that your UI is ready and to attach all the listeners to the LiveDatas
.
Next, you’ll change the UI a little so that it better fits the code you’re about to write.
Open activity_maps.xml and replace the include
tag with:
<include
android:id="@+id/container"
layout="@layout/layout_indicators" />
Here, you add a new container
id that you’ll use later to access the fields in the layout. You need one more step.
Switch to layout_indicators.xml and find the TextView
with id @+id/textTime
. Change it’s type to a Chronometer
, so that it looks like this:
<Chronometer
android:id="@+id/txtTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:fontFamily="monospace"
tools:text="45 minutes"
app:layout_constraintBottom_toBottomOf="@+id/txtTimeLabel"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/txtTimeLabel"
app:layout_constraintTop_toTopOf="@+id/txtTimeLabel" />
Here, you change the TextView
so that it tracks the elapsed time in a much easier way and displays it in the underlying TextView
.
Back to the IDE warnings. To make it happy, you need to add some code. Paste this snippet in MapsActivity
as class methods:
private fun startTracking() {
//1
binding.container.txtPace.text = ""
binding.container.txtDistance.text = ""
//2
binding.container.txtTime.base = SystemClock.elapsedRealtime()
binding.container.txtTime.start()
//3
map.clear()
//4
presenter.startTracking()
}
private fun stopTracking() {
presenter.stopTracking()
binding.container.txtTime.stop()
}
A few good things happen here:
- First, you reset the pace and distance fields since the user wants to record a new activity.
- Second, you set the time label to the current time and restart it. You do this because it only starts counting from the last moment recorded, so if you don’t reset it, you’ll have some weird data.
- Third, you clear the map. You don’t want to have old markers because you only care about the new ones coming in any minute.
- Last but not least, you ask the presenter to start tracking, and all the data will flow!
When you stop tracking, you stop all the presenter’s functions and also stop the time label. You don’t need to reset anything yet. This way, you also have a nice recap frame for your users.
You’re about to make the IDE unhappy once more! Right below these two methods, paste:
@SuppressLint("MissingPermission")
private fun updateUi(ui: Ui) {
//1
if (ui.currentLocation != null && ui.currentLocation != map.cameraPosition.target) {
map.isMyLocationEnabled = true
map.animateCamera(CameraUpdateFactory.newLatLngZoom(ui.currentLocation, 14f))
}
//2
binding.container.txtDistance.text = ui.formattedDistance
binding.container.txtPace.text = ui.formattedPace
//3
drawRoute(ui.userPath)
}
Here’s what you did:
- You check that the new position you get is not null and that it’s different from the position on the map. If you skip this step, you’ll experience crashes since the location could be null or weird flickers as you move the camera in and out from the same position. If the check passes, you move the camera to the new location, as you did earlier.
- Next, you bind the formatted data to the bindings.
- Finally, you draw the user’s path!
Scroll a little bit, and paste this code as another function of the MapsActivity
:
private fun drawRoute(locations: List<LatLng>) {
val polylineOptions = PolylineOptions()
map.clear()
val points = polylineOptions.points
points.addAll(locations)
map.addPolyline(polylineOptions)
}
This function is all you need to draw a line with several points on the UI.
- First, you create a
PolylineOptions
, an object that contains the points to draw on the map. - Then you clear everything on the map itself.
- Finally, you add all the new locations and plot the line on the map.
Done!
Well, not quite. There’s one more step to take.
Are you ready? Great!
Scroll to the onMapReady
method and change so that it looks like this:
override fun onMapReady(googleMap: GoogleMap) {
map = googleMap
//1
presenter.ui.observe(this) { ui ->
updateUi(ui)
}
//2
presenter.onMapLoaded()
map.uiSettings.isZoomControlsEnabled = true
}
First, you observe the data from the presenter so that you change the UI accordingly every time there’s an update. Second, you tell the presenter that the map loaded, so it can ask for the location permission if the user removed it.
Now, you really did it. It’s time to build your app and go out for a walk. You have to test it, don’t you?
Start your app and walk around. It’ll track you and display some data, more or less like this: