Taxonomy Icon

Mobile Development

In this tutorial, we are going to use Android SDK and the Kotlin language to build a simple run-tracking mobile app. This app should be able to track the user’s running data, including run time, duration, distance, and location. To accomplish that, we will access the device GPS and use it to show the user’s location on Google Maps. Afterward, the data should be backed up in the cloud so that the user doesn’t have to worry about losing their data. We’re going to use the IBM Cloudant NoSQL DB service to provide the backend for our app.

Now that we have an idea of what kind of application we are going to be building, let’s start with the development process.

What you’ll need to build your app

1

Creating a new Android project

As always, the first step when creating an Android app is to set up the development environment. Fortunately, it’s easy to do using the Android SDK. You simply have to download Android Studio and install it on your system. Once the installation and the initialization are done, you will have all of the necessary tools to start your Android project.

The next step is to create a new Android project using Android Studio. In the dialog menu that pops up, don’t forget to activate Kotlin support, because that’s the language we’re going to be using. We’re going with Kotlin instead of Java™ for our app because Kotlin requires much less code compared to Java, which results in faster code writing.

As for the API level, you should go with API level 15 because our app doesn’t require advanced features that exist in the latest SDK.

The last part in the project creation process is the main Activity of the app. For our run-tracking app, the main activity should show a list of all previous runs and a button to track a new run. So, with that in mind, let’s name it HomeActivity and use Basic Activity as the template for our initial Activity (see Figure 1).

Figure 1. New Activity template dialog
New Activity template dialog

And with that, we have a new Android project that is ready to be worked on.

All right, let’s talk about how our app will function. We have three separate Activities for our app:

  • An Activity that shows a list of the user’s previous runs
  • An Activity that tracks the user’s run data
  • An Activity that shows the detailed data of each run

We will use the Activity we just created, HomeActivity, as the Activity that shows a list of runs. However, we don’t have any run data to list yet, so let’s first create the Activity that will track the data.

2

Tracking run data

Create another Activity, and this time name it TrackActivity, with Empty Activity as the template. Now you should have two Activity files in our project folder, as you can see in Figure 2.

Figure 2. Activity files
Activity files

Before you start working on TrackActivity, you need a way for the user to navigate to this second Activity. To do that, on the TrackActivity file, simply add the following code to the fab object click listener:

fab.setOnClickListener {
startActivity(Intent(this, TrackActivity::class.java))
}

Now, when the Floating Action Button (FAB) on HomeActivity is clicked, the app will open TrackActivity.

2a

Tracking the run duration by creating a timer

Create another Activity, and this time name it TrackActivity, with Empty Activity as the template. Now you should have two Activity files in our project folder, as in Figure 2.

In the TrackActivity screen, there are two main things that we should track. The first is run duration and the second is the route of the run. Let’s start with tracking run duration.

To track run duration, add a button that the user can use to toggle the tracking process as well as a timer that counts how long the run is taking place. We can add the necessary interface component to res/layout/activity_track.xml with the XML lines in the following listing.

<TextView
   android:id="@+id/text_duration"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_marginBottom="4dp"
   android:layout_marginLeft="16dp"
   android:layout_marginStart="16dp"
   android:text="00:00:000"
   android:textSize="36sp"
   android:textStyle="bold"
   app:layout_constraintBottom_toTopOf="@+id/button_track"
   app:layout_constraintStart_toStartOf="parent" />

<Button
   android:id="@+id/button_track"
   android:layout_width="0dp"
   android:layout_height="wrap_content"
   android:layout_marginBottom="16dp"
   android:layout_marginLeft="16dp"
   android:layout_marginRight="16dp"
   android:enabled="false"
   android:paddingBottom="20dp"
   android:paddingTop="20dp"
   android:text="Start"
   android:textSize="20sp"
   app:layout_constraintBottom_toBottomOf="parent"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toStartOf="parent" />

If you run the app and open TrackActivity, it should show a user interface similar to Figure 3.

Figure 3. Timer user interface
Timer user interface

Because our TrackActivity has a couple of user interface elements, let’s access them so that we can configure our user interface further. Get the interface objects using the findViewById function, and then add a listener to the button to handle starting and stopping the timer. To accomplish that, add the code in the following listing to the TrackActivity class:

class TrackActivity : AppCompatActivity() {
   private var mTracking: Boolean = false

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_track)
       setSupportActionBar(toolbar)

       var buttonTrack: Button = findViewById(R.id.button_track)
       var textDuration: TextView = findViewById(R.id.text_duration)

       buttonTrack.setOnClickListener {
           if (mTracking) {
               //Stop tracking process
           } else {
               //Start tracking process              
           }

           mTracking = !mTracking
       }
   }
}

Now that you have the text user interface element, implement the timer system on it. Implementing a timer in an Android app is a bit complicated because Android apps are usually functioning based on events instead of real-time. So, to get a working timer, we’re going to have to rely on multithreading and use the Handler class, as can be seen in the following code listing:

var duration: Long = 0

var handler: Handler = Handler()

buttonTrack.setOnClickListener {
   if (mTracking) {
       handler.removeCallbacksAndMessages(null)
   } else {
       startTime = System.currentTimeMillis()

       handler.post(object: Runnable {
           override fun run() {
               duration = System.currentTimeMillis() - startTime
               var formatter = SimpleDateFormat("mm:ss:SSS")
               textDuration.text = formatter.format(Date(duration))

               handler.postDelayed(this, 20)
           }
       })    
   }
}

So, how does our timer system work? Before starting the Handler loop, it first records the system’s current millisecond with the System.currentTimeMillis function. Then, inside the Handler loop, it simply counts the current time difference to get tracking duration and update the text. After that, we call the same Handler function again with a delay of 20 milliseconds. And lastly, don’t forget to stop the handler with the removeCallbacksAndMessages function when user stops the tracking.

2b

Displaying a map to use to track the user’s position

With the time part of the tracking covered, we’ll move on to the positional part. For positional tracking, there are two interface components that we need to add: a Google Maps component (which class is called MapView in the codes) and a distance counter. To do that, add the following lines to the activity_track.xml file:

<TextView
   android:id="@+id/text_distance"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:layout_marginBottom="4dp"
   android:layout_marginEnd="16dp"
   android:layout_marginRight="16dp"
   android:text="0.00 m"
   android:textSize="36sp"
   android:textStyle="bold"
   app:layout_constraintBottom_toTopOf="@+id/button_track"
   app:layout_constraintEnd_toEndOf="parent" />

<com.google.android.gms.maps.MapView
   android:id="@+id/map"
   android:layout_width="0dp"
   android:layout_height="0dp"
   android:layout_marginBottom="8dp"
   android:layout_marginEnd="16dp"
   android:layout_marginLeft="16dp"
   android:layout_marginRight="16dp"
   android:layout_marginStart="16dp"
   android:layout_marginTop="16dp"
   app:layout_constraintBottom_toTopOf="@+id/text_duration"
   app:layout_constraintEnd_toEndOf="parent"
   app:layout_constraintStart_toStartOf="parent"
   app:layout_constraintTop_toTopOf="parent" />

After adding MapView and a distance counter to TrackActivity, the Activity should look like Figure 4.

Figure 4. MapView on TrackActivity
MapView on TrackActivity

MapView isn’t included by default with the Android SDK, but fortunately Android Studio automatically downloads the required library and adds the correct dependencies to the project. However, to use MapView further, we will have to register a Google Map API key. To do that, take the Google Map API we created earlier and register it on app/manifests/AndroidManifest.xml by inserting the following lines below the <application> tag:

<meta-data
   android:name="com.google.android.geo.API_KEY"
   android:value="INSERT API KEY HERE" />

To properly use the MapView and to be able to track the user’s position, we have to ask permission from the user. There are two parts in this process; the first one is simply to add the required permission to the app manifest, which we can do by adding the following lines:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

The other part is to actively request the user for permission when the app is running. In this app, the best place to ask for permission is right before the user enters TrackActivity. So, let’s get back to HomeActivity for a moment and modify the FAB click listener to include permission checking, as can be seen in the following listing.

var permission = Manifest.permission.ACCESS_FINE_LOCATION
var granted = PackageManager.PERMISSION_GRANTED

if (ActivityCompat.checkSelfPermission(this, permission) == granted) {
   startActivity(Intent(this, TrackActivity::class.java))
} else {
   ActivityCompat.requestPermissions(this, arrayOf(permission), 1)
}

Now, whenever we try to open TrackActivity in our app, it will ask the user for location permission if it hasn’t been given before (see Figure 5). This way, we won’t encounter any permission issue inside TrackActivity.

Figure 5. Request location permission dialog
Request location permission dialog

With the permission part taken care of, we should put our focus back on the MapView. Unlike other user interface elements, integrating MapView to our Activity requires some additional work. For MapView to work correctly, we have to handle its lifecycle alongside the Activity’s lifecycle. To do that, we need to override the lifecycle functions like onStart, onStop, and so on on TrackActivity, and then call the corresponding functions from MapView, as the following listing shows:

override fun onStart() {
   super.onStart()
   findViewById<MapView>(R.id.map).onStart()
}

override fun onResume() {
   super.onResume()
   findViewById<MapView>(R.id.map).onResume()
}

override fun onPause() {
   findViewById<MapView>(R.id.map).onPause()
   super.onPause()
}

override fun onStop() {
   super.onStop()
   findViewById<MapView>(R.id.map).onStop()
}

override fun onDestroy() {
   findViewById<MapView>(R.id.map).onDestroy()
   super.onDestroy()
}

override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)
   findViewById<MapView>(R.id.map).onSaveInstanceState(outState)
}

override fun onLowMemory() {
   super.onLowMemory()
   findViewById<MapView>(R.id.map).onLowMemory()
}

Afterward, you should initialize the MapView and configure it further after the initialization is finished. For this app, we want the user to not be able to interact with the MapView, so we will disable various control options on it. All of that can be done by adding the code in following listing to the TrackActivity class:

var mapView: MapView = findViewById(R.id.map)

var map: GoogleMap? = null

mapView.onCreate(savedInstanceState)
mapView.getMapAsync(object : OnMapReadyCallback {
   override fun onMapReady(googleMap: GoogleMap?) {
       map = googleMap;

       if (map != null) {
           map!!.setMinZoomPreference(16f)
           map!!.isMyLocationEnabled = true
           map!!.uiSettings.isZoomControlsEnabled = false
           map!!.uiSettings.isScrollGesturesEnabled = false
           map!!.uiSettings.isMyLocationButtonEnabled = false
       }
   }

})
2c

Updating the map by tracking the user’s position

With the MapView implemented, we can move on to tracking the user’s position, or the route of the run. We can get the user’s continuous position by using the FusedLocationProviderClient API and by calling the requestLocationUpdates function. Before calling that function, we should configure what kind of location data that our app needs and how often we want it. We can do that by instantiating a RequestLocation object and then specifying the interval and priority parameter.

Then, after we get the position data, we should update the MapView position with map.moveCamera() function. We should also save the latest position data and compare it to the previous data using Location.distanceBetween function so that we can calculate how far has the user moved since the last position update.

And of course, we should stop the continuous location request by calling the removeLocationUpdates function when the user switches off the tracking process. We can implement all of these functionalities with the following lines of code:

var textDistance: TextView = findViewById(R.id.text_distance)

var duration: Long = 0
var startCoordinate: LatLng? = null
var finishCoordinate: LatLng? = null

var locationListener: LocationCallback = object : LocationCallback(){
   override fun onLocationResult(result: LocationResult?) {
       if (result != null) {
           var previousCoordinate: LatLng? = finishCoordinate

           var lastLoc = result.lastLocation
           finishCoordinate = LatLng(lastLoc.latitude, lastLoc.longitude)

           if (startCoordinate == null) {
               startCoordinate = finishCoordinate;
           }

           if (map != null) {
               map!!.moveCamera(CameraUpdateFactory.newLatLng(finishCoordinate))
           }

           if (previousCoordinate != null) {
               var results: FloatArray = floatArrayOf(0.0f)
               Location.distanceBetween(
                       previousCoordinate.latitude,
                       previousCoordinate.longitude,
                       finishCoordinate!!.latitude,
                       finishCoordinate!!.longitude,
                       results)

               distance += results[0]
               textDistance.text = String.format("%.2f m", distance)
           }
       }
   }
}

buttonTrack.setOnClickListener {
   if (mTracking) {       
       var locationProvider = LocationServices.getFusedLocationProviderClient(this)
       locationProvider.removeLocationUpdates(locationListener)
   } else { 
       distance = 0f
       startCoordinate = null
       finishCoordinate = null       

       var request: LocationRequest = LocationRequest().apply {
           priority = LocationRequest.PRIORITY_HIGH_ACCURACY
           fastestInterval = 1000
           interval = 10000
       }

       var builder = LocationSettingsRequest.Builder()
       builder.addLocationRequest(request)
       LocationServices.getSettingsClient(this).checkLocationSettings(builder.build())

       var provider = LocationServices.getFusedLocationProviderClient(this)
       provider.requestLocationUpdates(request, locationListener, Looper.myLooper())
   }
}

And that’s it for the run tracking functionality. If you run the app and try to track a run, it should display a screen similar to the one in Figure 6.

Figure 6. TrackActivity tracking a run
TrackActivity tracking a run
3

Displaying run data

Now that our app can capture the data of a run, it should also be able to display it again, so let’s implement data display functionality for our app. To do that, we should create our third Activity that we can use to show the details of a run.

Before we create this Activity, let’s first create a class that holds the data of a run and is capable of exporting and importing a HashMap of said data. The latter point is important, because we’re going to move the data around between activities and between client-server as a HashMap. We’ll call this class RunData, and the full implementation of that class can be in the RunData.kt file in my runlover Github repo that contains all of the code for this project.

Create another activity called DetailActivity that uses Empty Activity as the template. Users should be able to navigate to this Activity by clicking on a save button in TrackActivity after they finish tracking their run. To achieve that, insert the following code to the button click listener so that it opens DetailActivity and sends along the recorded run data:

var buttonSave: Button = findViewById(R.id.button_save)

buttonSave.setOnClickListener {
   var data: HashMap<String, Any> = RunData.CreateMap(
         startTime, duration, distance, startCoordinate, finishCoordinate)

   var detailIntent: Intent = Intent(this, DetailActivity::class.java)
   detailIntent.putExtra(DetailActivity.EXTRA_DATA, data)

   finish()
   startActivity(detailIntent)
}

In DetailActivity, you should use the data that was sent from TrackActivity and display it on the screen. To accomplish that, the first step is to retrieve the data and construct a RunData object from it, which can be done by adding the following code to DetailActivity:

class DetailActivity : AppCompatActivity() {
   companion object {
       const val EXTRA_DATA = "data"
   }

   private lateinit var mData: RunData

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_detail)
       setSupportActionBar(toolbar)

       var extra = intent.getSerializableExtra(EXTRA_DATA) as Map<String, Any>
       mData = RunData(extra)
   }
}

Before you can display the data, you need to add a few interface components on the Activity. The layout for DetailActivity is similar to TrackActivity, with a MapView and a couple of text views to display the data of a run. You can use the XML codes in my activity_detail.xml file to set up the necessary interface to DetailActivity.

Now that you have the user interface for DetailActivity, you can add the following code to DetailActivity and make it show the correct data:

var textDate: TextView = findViewById(R.id.text_date)
var textDuration: TextView = findViewById(R.id.text_duration)
var textDistance: TextView = findViewById(R.id.text_distance)

var dateFormatter = SimpleDateFormat("dd/MM/yyyy")
var timeFormatter = SimpleDateFormat("mm:ss:SSS")
var durationString = timeFormatter.format(Date(mData.getDurationInMillis()))

textDate.text = dateFormatter.format(Date(mData.getDateInMillis()))
textDistance.text = String.format("Distance: %.2f m", mData.getDistance())
textDuration.text = "Duration: $durationString"

If you run the app and save the run data, the app should look similar to Figure 7.

Figure 7. DetailActivity showing run detail
DetailActivity showing run detail

And with that, we’ve finished implementing the data display functionality of the app.

4

Connecting to the cloud

The app shouldn’t only capture and display the user’s run data, it should also store it to the cloud. To add this functionality, first you need to set up the backend server that will store this data.

Instead of creating your own backend system, we’ll use the IBM Cloudant NoSQL DB service as the backend for our app. By using this service, you don’t have to worry about provisioning or configuring the remote server, and you can fully focus on building the mobile app.

4a

Setting up the Cloudant NoSQL DB service

Setting up the Cloudant NoSQL DB service is simple. First, log in to your IBM Cloud account and use it to create an instance of the Cloudant NoSQL DB service from here. Then, after the service is created, launch the service dashboard and access the Databases menu on the left. You need at least one database here, so create a new one using the Create Database button at the top, and remember the name of this new database.

Figure 8. Cloudant NoSQL DB service dashboard
Cloudant NoSQL DB service dashboard

That’s it for setting up the service. The backend is ready to retrieve and store data. That said, don’t close the dashboard just yet, because we’re going to need it again very soon.

4b

Adding Cloudant Sync to our Android project

With our remote database ready, we need a way to access it from the mobile app. Fortunately, Cloudant provides an Android library called Cloudant Sync that provides that access. To add Cloudant Sync library to our project, open up build.gradle(Project) and insert the following lines:

allprojects {
   repositories {
       mavenLocal()
       mavenCentral()
   }
}

Insert the following lines to build.gradle(App):

dependencies {   
   implementation 'com.cloudant:cloudant-sync-datastore-android:latest.release'
}

The Cloudant Sync library has two major functionalities that we are using in our project. The first one is the DocumentStore API that handles storing and retrieving the data locally on the device, which allows our app to operate even when offline. The other one is the Replicator API that handles replicating or syncing local and remote data. Using these two functionalities correctly is key in connecting our app with the backend.

To connect our mobile app with our Cloudant NoSQL DB service, we have to provide the library with the proper URL. You can get this URL from the IBM Cloud dashboard we opened earlier. Go to the Service credentials menu on the left, and open the single credential entry in the list; the URL address that we need should be written there.

4c

Sending data to the backend

We have everything we need to connect to the backend, so it’s time to implement the cloud functionality in our app. In the save button, click listener on TrackActivity, add the following code to save the run data locally, and then upload it to the remote database.

buttonSave.setOnClickListener {
   var data: HashMap<String, Any> = RunData.CreateMap(
         startTime, duration, distance, startCoordinate, finishCoordinate)

   var document: DocumentRevision = DocumentRevision()
   document.body = DocumentBodyFactory.create(data)

   var directory = getDir("DocumentStore", Context.MODE_PRIVATE)
   var store: DocumentStore = DocumentStore.getInstance(directory)
   store.database().create(document)

   var databaseURI: URI = URI("$BACKEND_URL/$DATABASE_NAME")
   var uploader = ReplicatorBuilder.push().from(store).to(databaseURI).build()
   uploader.start()
}

We can store run data on the backend by first creating a DocumentRevision object containing the run data. We then store this object on a local file by calling the database.create function of the DocumentStore API. Afterward, sync it to the remote database by calling the push function of the Replicator API. That’s all you need to do to send data to the backend.

4d

Retrieving data from the backend

Our app is now capable of storing data in the cloud, but why do that if we’re not going to retrieve the data again? That’s exactly what we’re going to work on next. Let’s go back to the first Activity we created, HomeActivity, and have it display a list of all runs the user has done.

Before we can display that list, we first need to retrieve all the data. Add the following code to HomeActivity to read all the run data that was saved locally by calling database.read function of the DocumentStore API:

class HomeActivity : AppCompatActivity() {
   private var mStore: DocumentStore? = null

   private var mHistory: ArrayList<RunData> = ArrayList<RunData>()

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContentView(R.layout.activity_home)
       setSupportActionBar(toolbar)  

       var directory = getDir("DocumentStore", Context.MODE_PRIVATE)
       mStore = DocumentStore.getInstance(directory)       
   }

   override fun onStart() {
       super.onStart()

       readHistoryData()
   }

   private fun readHistoryData() {
       if (mStore != null) {
           var database = mStore!!.database();

           if (mHistory.count() != database.documentCount) {
               mHistory.clear()

               var documentList = database.read(0, database.documentCount, true)
               for (document in documentList) {
                   mHistory.add(RunData(document.body.asMap()))
               }
           }
       }
   }
}

We also need to check our remote database and download any new run data stored there. To accomplish that, simply call the pull function of the Replicator API and create listener functions to detect when the download is finished, as shown in the following listing:

override fun onCreate(savedInstanceState: Bundle?) {
   //Other initialization here

   var databaseURI: URI = URI("$BACKEND_URL/$DATABASE_NAME")
   mDownloader = ReplicatorBuilder.pull().from(databaseURI).to(mStore).build()
   mDownloader!!.eventBus.register(this)
   mDownloader!!.start()
}

@Subscribe
public fun onComplete(event: ReplicationCompleted) {
   mDownloader!!.eventBus.unregister(this)
   mDownloader = null

   readHistoryData()
}

@Subscribe
public fun onError(event: ReplicationErrored) {
   mDownloader!!.eventBus.unregister(this)
   mDownloader = null
}
5

Displaying a list of run data

Right now, we have a working TrackActivity that tracks the user’s run data and a working DetailActivity that shows a user’s run data. All that’s left is an Activity that shows all the previous runs that the app has tracked. So, let’s get back to HomeActivity and make it show a list of run data.

5a

Preparing the user interface

We need a user interface on HomeActivity to display all of the data we have captured. To accomplish that, insert a RecyclerView to the Activity by adding the following lines to res/layout/content_home.xml:

<android.support.v7.widget.RecyclerView
   android:id="@+id/recycler_history"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:listitem="@layout/view_history"/>

RecyclerView is essentially an interface component to display multiple items in list, grid, or other formats. To define how each item in the list would look, we have to create a separate layout file for those items. Use this XML file, and put it on the layout folder so that you can later use it for the data list.

5b

Handling and displaying a list

Just like how a RecyclerView has two separate layout information, displaying data on it also requires two separate classes. One of these classes is RecyclerView.Adapter, which handles the full list of items to be displayed. The other one is RecyclerView.ViewHolder, which should handle how each item would be presented.

Let’s start by creating a new class called HistoryAdapter that inherits from RecyclerView.Adapter. Then in this class, create an internal class called HistoryViewHolder that inherits from RecyclerView.ViewHolder. In HistoryViewHolder, query each item interface component so that it can display the corresponding data later, as shown in the following listing:

class HistoryAdapter(): RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder>() {

   class HistoryViewHolder constructor(view: View) : RecyclerView.ViewHolder(view) {
       public var textDate: TextView
       public var textDuration: TextView
       public var textDistance: TextView

       init {
           textDate = view.findViewById(R.id.text_date)
           textDuration = view.findViewById(R.id.text_duration)
           textDistance = view.findViewById(R.id.text_distance)
       }
   }
}

With ViewHolder ready, you can now work on the rest of the HistoryAdapter class. Add the following code to override several functions in the class so that it returns the correct result:

class HistoryAdapter(private var mHistory: List<RunData>):
       RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder>() {

   override fun onCreateViewHolder(parent: ViewGroup, vType: Int): HistoryViewHolder {
       var inflater: LayoutInflater = LayoutInflater.from(parent.context)
       var view: View = inflater.inflate(R.layout.view_history, parent, false)

       return HistoryViewHolder(view)
   }

   override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) {
       var distance = mHistory[position].getDistance()
       var duration = Date(mHistory[position].getDurationInMillis())
       var date = Date(mHistory[position].getDateInMillis())

       holder.textDistance.text = String.format("%.2f m", distance)
       holder.textDuration.text = SimpleDateFormat("mm:ss:SSS").format(duration)
       holder.textDate.text = SimpleDateFormat("dd/MM/yyyy").format(date)
   }

   override fun getItemCount(): Int {
       return mHistory.count()
   }
}

With the HistoryAdapter class and the HistoryViewHolder class ready, our RecyclerView should be able to properly show data. To have it show all the run data we have tracked, we must provide it with that data. Insert the following lines of code to HomeActivity to access RecyclerView and update it with the user’s run data:

override fun onCreate(savedInstanceState: Bundle?) {
   var orientation = DividerItemDecoration.VERTICAL
   var recycler: RecyclerView = findViewById(R.id.recycler_history)   
   recycler.addItemDecoration(DividerItemDecoration(this, orientation))
   recycler.setHasFixedSize(true)

   recycler.layoutManager = LinearLayoutManager(this)
   recycler.adapter = HistoryAdapter(mHistory)
}

private fun readHistoryData() {
   //Reading new data

   var recycler: RecyclerView = findViewById(R.id.recycler_history)
   recycler.adapter.notifyDataSetChanged()
   recycler.recycledViewPool.clear()
   recycler.invalidate()
}

And with that, our app should properly display a list of the user’s run data on HomeActivity, as can be seen in Figure 9.

Figure 9. HomeActivity showing a list of runs
HomeActivity showing a list of runs
5c

Creating a clickable list

With the list ready, there is one last thing that we have to do. We should provide the user with a way to navigate to DetailActivity when they click on an item in the run data list. To achieve that, first add a click listener to the HistoryViewHolder by using the code in the following listing:

class HistoryAdapter(private var mHistory: List<RunData>,
                    private var mListener: OnItemClickListener):
       RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder>() {

   interface OnItemClickListener {
       fun onItemClick(data: RunData)
   }

   class HistoryViewHolder constructor(view: View) : RecyclerView.ViewHolder(view) {

       public fun setOnClickListener(listener: OnItemClickListener, data: RunData) {
           itemView.setOnClickListener {
               listener.onItemClick(data)
           }
       }
   }

   override fun onBindViewHolder(holder: HistoryViewHolder, position: Int) {
       //Displaying data on TextView

       holder.setOnClickListener(mListener, mHistory[position])
   }
}

Then, we add the activity navigation functionality when we’re constructing the HistoryAdapter on HomeActivity with these lines:

var clickListener = object : HistoryAdapter.OnItemClickListener{
   override fun onItemClick(data: RunData) {
       var dataMap = RunData.CreateMap(
               data.getDateInMillis(),
               data.getDurationInMillis(),
               data.getDistance(),
               data.getStartCoordinate(),
               data.getFinishCoordinate())

       var detailIntent = Intent(applicationContext, DetailActivity::class.java)
       detailIntent.putExtra(DetailActivity.EXTRA_DATA, dataMap)

       startActivity(detailIntent)
   }
}

recycler.adapter = HistoryAdapter(mHistory, clickListener)

If you run the app, you should be able to quickly access the detail of each run by tapping the item on the list.

6

Build and run the app

We’ve implemented all of the required functionalities, so all that’s left to do is to build the app with the Build > Build APK(s) menu.

That’s it. You now have a fully functioning mobile app that can track user’s run data and store it in the cloud.

There are some minor details like displaying position marker on the map that aren’t included here, but you can check out the full project in my GitHub repo for yourself.

Summary and next steps

Now that you know how to build an Android application from scratch, there are many options that you can try next. You can build a new app that interests you, or you can learn further by adding even more features to this app. It could be as simple as adding a delete run feature, or it could be as complex as adding an authorization system.

Good luck!