Training summary
After a training session you should show a summary to the user with a detail of their calories, heart rate, etc.
In this tutorial you will learn how to retrieve training information, summaries and records from Database while the training is being uploaded.
Contents
Coding the UI
This section covers 2 activities:
- HomeActivity - The home UI of the app.
- TrainingTypeActivity - Will display a list of training types.
The file structure should look like below:
your.package.name/
|-- ui/
|-- training/
|-- summary/
|-- TrainingSummaryActivity
Create the UI for each activity.
The activity_training_summary.xml file will show basic information, summaries and the amount of individual records.
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.training.summary.TrainingSummaryActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="@string/training_summary"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"/>
<include
android:id="@+id/progress"
layout="@layout/ui_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/basic_information"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textSize="18sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/training_name"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/training_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="Individual training"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/training_type"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/training_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="Running"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/start_date"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/start_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="March 16, 2022 10:56 am"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/end_date"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/end_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="March 16, 2022 11:56 am"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/summaries"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textSize="18sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/duration"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="1500"/>
<TextView
android:id="@+id/steps_types"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/steps"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/steps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="2388"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/calories"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/calories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="1500"/>
<TextView
android:id="@+id/effort"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="@string/effort_min_avg_max_placeholder"/>
<TextView
android:id="@+id/heart_rate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="@string/hr_min_avg_max_placeholder"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:text="@string/individual_records"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
android:textSize="18sp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/number_of_hr_records"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/heart_rate_records"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="1500"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/number_of_steps_records"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"/>
<TextView
android:id="@+id/steps_records"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
tools:text="1500"/>
</LinearLayout>
</ScrollView>
The TrainingSummaryKeys has constants that will be used to pass parameters from TrainingSoloActivity
to TrainingSummaryActivity.
package com.rookmotion.rooktraining.ui.training
object TrainingKeys {
const val TRAINING_TYPE_SELECTED = "training_type_selected"
}
Finally, make TrainingSummaryActivity portrait orientation only.
<activity
android:name=".ui.training.summary.TrainingSummaryActivity"
android:exported="false"
android:screenOrientation="portrait"
tools:ignore="LockedOrientationActivity"/>
- This is required to reduce the tutorial length, the summary logic will be implemented in a ViewModel to keep state between configuration changes.
Coding the repositories
The file structure should look like below:
your.package.name/
|-- data/
|-- repository/
|-- TrainingSummaryRepository
Create an TrainingSummaryRepository class with an RM instance as a constructor parameter.
package com.rookmotion.rooktraining.data.repository
import com.rookmotion.app.sdk.RM
class TrainingSummaryRepository(private val rm: RM) {
}
To get training information from Database call getTrainingInformationFromDatabase, and provide the start timestamp in
the format YYYY-MM-DD HH:MM:SS.
There's also a getTrainingTypesFromDatabase function if you only want to get local training types.
fun getTrainingInformation(
start: String,
onSuccess: (RMTrainingModel) -> Unit,
onError: (String) -> Unit
) {
rm.getTrainingInformationFromDatabase(start) { rmResponse, rmTraining ->
if (rmResponse.isSuccess && rmTraining != null) {
onSuccess(rmTraining)
} else {
onError(rmResponse.message)
}
}
}
To get training summaries from Database call getSummariesFromDatabase, and provide the start timestamp in the format
YYYY-MM-DD HH:MM:SS.
fun getHeartRateRecords(
start: String,
onSuccess: (List<HeartRateDerivedData>) -> Unit,
onError: (String) -> Unit
) {
rm.getHeartRateRecordsFromDatabase(start) { rmResponse, records ->
if (rmResponse.isSuccess && records != null) {
onSuccess(records)
} else {
onError(rmResponse.message)
}
}
}
fun getStepsRecords(
start: String,
onSuccess: (List<StepsDerivedData>) -> Unit,
onError: (String) -> Unit
) {
rm.getStepsRecordsFromDatabase(start) { rmResponse, records ->
if (rmResponse.isSuccess && records != null) {
onSuccess(records)
} else {
onError(rmResponse.message)
}
}
}
To get training heart rate records from Database call getHeartRateRecordsFromDatabase, and provide the start timestamp
in the format YYYY-MM-DD HH:MM:SS.
fun getHeartRateRecords(
start: String,
onSuccess: (List<HeartRateDerivedData>) -> Unit,
onError: (String) -> Unit
) {
rm.getHeartRateRecordsFromDatabase(start) { rmResponse, records ->
if (rmResponse.isSuccess && records != null) {
onSuccess(records)
} else {
onError(rmResponse.message)
}
}
}
To get training steps records from Database call getStepsRecords, and provide the start timestamp in the format
YYYY-MM-DD HH:MM:SS.
fun getStepsRecords(
start: String,
onSuccess: (List<StepsDerivedData>) -> Unit,
onError: (String) -> Unit
) {
rm.getStepsRecordsFromDatabase(start) { rmResponse, records ->
if (rmResponse.isSuccess && records != null) {
onSuccess(records)
} else {
onError(rmResponse.message)
}
}
}
Finally, create an instance of the repositories in your.package.name/rm/RMServiceLocator
class RMServiceLocator(context: Context) {
// Other configuration
val trainingSummaryRepository by lazy { TrainingSummaryRepository(rm) }
}
Coding the ViewModels
This section covers one view model:
- TrainingSummaryRepository - Used in
TrainingSummaryActivityto handle all summary states.
The file structure should look like below:
your.package.name/
|-- state/
|-- TrainingSummaryViewModel
Create an TrainingSummaryViewModel class extending from ViewModel with an TrainingSummaryRepository instance as a
constructor parameter.
package com.rookmotion.rooktraining.state
import androidx.lifecycle.ViewModel
import com.rookmotion.rooktraining.data.repository.TrainingSummaryRepository
class TrainingSummaryViewModel(
private val trainingSummaryRepository: TrainingSummaryRepository
) : ViewModel() {
}
Create a livedata object to hold trainingType state.
private val _trainingSummaryState = MutableLiveData<TrainingSummaryState>()
val trainingSummaryState: LiveData<TrainingSummaryState> get() = _trainingSummaryState
The training data will be retrieved in succession:
- Get training information
- Get summaries
- Get heart rate records
- Get steps records
- Emit the state
fun getTraining(start: String) {
_trainingSummaryState.value = TrainingSummaryState.Loading
trainingSummaryRepository.getTrainingInformation(
start = start,
onSuccess = { getSummaries(start, it) },
onError = { _trainingSummaryState.value = TrainingSummaryState.Error(it) }
)
}
private fun getSummaries(start: String, training: RMTrainingModel) {
trainingSummaryRepository.getSummaries(
start = start,
onSuccess = { getHeartRateRecords(start, training, it) },
onError = { _trainingSummaryState.value = TrainingSummaryState.Error(it) }
)
}
private fun getHeartRateRecords(start: String, training: RMTrainingModel, summary: RMSummary) {
trainingSummaryRepository.getHeartRateRecords(
start = start,
onSuccess = { getStepsRecords(start, training, summary, it) },
onError = { _trainingSummaryState.value = TrainingSummaryState.Error(it) }
)
}
private fun getStepsRecords(
start: String,
training: RMTrainingModel,
summary: RMSummary,
heartRateRecords: List<HeartRateDerivedData>,
) {
trainingSummaryRepository.getStepsRecords(
start = start,
onSuccess = {
_trainingSummaryState.value = TrainingSummaryState.Success(
training = training,
summary = summary,
heartRateRecords = heartRateRecords,
stepsRecords = it,
)
},
onError = { _trainingSummaryState.value = TrainingSummaryState.Error(it) }
)
}
The trainingSummaryState uses a sealed class like the following:
sealed class TrainingSummaryState {
object None : TrainingSummaryState()
object Loading : TrainingSummaryState()
class Error(val message: String) : TrainingSummaryState()
class Success(
val training: RMTrainingModel,
val summary: RMSummary,
val heartRateRecords: List<HeartRateDerivedData> = emptyList(),
val stepsRecords: List<StepsDerivedData> = emptyList()
) : TrainingSummaryState()
}
Finally, add the view models to the RMViewModelFactory's create function.
override fun <T : ViewModel> create(modelClass: Class<T>): T {
// Other conditions
if (modelClass.isAssignableFrom(TrainingSummaryViewModel::class.java)) {
return TrainingSummaryViewModel(rmServiceLocator.trainingSummaryRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
Integrating ViewModel and UI
TrainingSoloActivity
Go to the finishTraining function from previous tutorial, create and launch an intent to TrainingSummaryActivity
providing the start, training and steps type
private fun finishTraining() {
val startTime = trainingSoloViewModel.finishTraining()
val trainingType = trainingSoloViewModel.trainer.trainingType
val stepsType = trainingSoloViewModel.getStepsType()
val intent = Intent(this, TrainingSummaryActivity::class.java).apply {
putExtra(TrainingSummaryKeys.START, startTime)
putExtra(TrainingSummaryKeys.TRAINING_TYPE, trainingType.trainingName)
putExtra(TrainingSummaryKeys.STEPS_TYPE, stepsType.name)
}
startActivity(intent)
finish()
}
TrainingSummaryActivity
Create an TrainingSummaryViewModel instance
class TrainingSummaryActivity : AppCompatActivity() {
private val trainingSummaryViewModel by viewModels<TrainingSummaryViewModel> {
RMViewModelFactory(rmLocator)
}
}
In the onCreate function call the init functions
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityTrainingSummaryBinding.inflate(layoutInflater)
setContentView(binding.root)
initExtras()
initState()
}
In initExtras retrieve the start, training and steps types, display the trainingType use stepsType to show or hide
steps information, finally use start to call getTraining.
private fun initExtras() {
val start = intent.extras?.getString(TrainingSummaryKeys.START) ?: ""
val trainingType = intent.extras?.getString(TrainingSummaryKeys.TRAINING_TYPE) ?: ""
val stepsType = StepsType.valueOf(
intent.getStringExtra(TrainingSummaryKeys.STEPS_TYPE) ?: "NONE"
)
binding.trainingType.text = trainingType
if (stepsType != StepsType.NONE) {
binding.stepsTypes.text = stepsType.name
binding.stepsTypes.isVisible = true
binding.steps.isVisible = true
} else {
binding.stepsTypes.isVisible = false
binding.steps.isVisible = false
}
trainingSummaryViewModel.getTraining(start)
}
Consume the trainingSummaryState livedata and implement all cases:
- None - Log.
- Loading - Show progress.
- Success - Hide progress, fill training, summary and records info.
- Error - Hide progress, show error.
private fun initState() {
trainingSummaryViewModel.trainingSummaryState.observe(this) {
when (it) {
TrainingSummaryViewModel.TrainingSummaryState.None -> Timber.i("trainingSummaryState.None")
TrainingSummaryViewModel.TrainingSummaryState.Loading -> {
binding.progress.root.isVisible = true
}
is TrainingSummaryViewModel.TrainingSummaryState.Success -> {
showTrainingInfo(it.training)
showSummaryInfo(it.summary)
showRecordsInfo(it.heartRateRecords, it.stepsRecords)
binding.progress.root.isVisible = false
}
is TrainingSummaryViewModel.TrainingSummaryState.Error -> {
binding.root.snackLong(it.message, getString(R.string.retry)) {
trainingSummaryViewModel.getTraining(
start = intent.extras?.getString(TrainingSummaryKeys.START) ?: ""
)
}
binding.progress.root.isVisible = false
}
}
}
}
Use deviceType to display if this is an individual training (app) or a remote training (remote).
- The properties
startandendare in UTC you'll need to adjust them to your users time zone. More about UTC in Getting familiar tutorial.
private fun showTrainingInfo(training: RMTrainingModel) {
binding.trainingName.text =
if (training.deviceType == "app") getString(R.string.individual_training)
else getString(R.string.remote_training)
binding.startDate.text = UTCConverter.getDateTime(training.start, true)?.toString()
binding.endDate.text = UTCConverter.getDateTime(training.end, true)?.toString()
}
The UTCConverter object is available in our sdk-utils library.
RMSummary contains total, min, avg, max and per-zone values to display useful insights.
- The property
startis in UTC you'll need to adjust it to your users time zone. More about UTC in Getting familiar tutorial.
private fun showSummaryInfo(summary: RMSummary) {
binding.duration.text = TimeFormatter.formatSecondsToHHMMSS(summary.totalTime)
binding.steps.text = summary.totalSteps.toString()
binding.calories.text = summary.totalCalories.roundToInt().toString()
binding.effort.text = getString(
R.string.effort_min_avg_max_placeholder,
summary.minEffort.roundToInt().toString(),
summary.avgEffort.roundToInt().toString(),
summary.maxEffort.roundToInt().toString()
)
binding.heartRate.text = getString(
R.string.hr_min_avg_max_placeholder,
summary.minHeartRate.roundToInt().toString(),
summary.avgHeartRate.roundToInt().toString(),
summary.maxHeartRate.roundToInt().toString()
)
}
A list of all properties in RMSummary:
String start
float totalTime
float zoneZeroTime
float zoneOneTime
float zoneTwoTime
float zoneThreeTime
float zoneFourTime
float zoneFiveTime
float totalCalories
float zoneOneCalories
float zoneTwoCalories
float zoneThreeCalories
float zoneFourCalories
float zoneFiveCalories
long totalSteps
long zoneOneSteps
long zoneTwoSteps
long zoneThreeSteps
long zoneFourSteps
long zoneFiveSteps
float zoneOneCadence
float zoneTwoCadence
float zoneThreeCadence
float zoneFourCadence
float zoneFiveCadence
float minHeartRate
float maxHeartRate
float avgHeartRate
float avgEffort
float minEffort
float maxEffort
float avgCadence
float maxCadence
float minCadence
Each HeartRateDerivedData and StepsDerivedData represent a single record. Their properties are exclusive of each
type of record except for:
- id - Database primary key, you'll never need to used it, ignore.
- start - The start timestamp of the training this record belongs to (foreign key).
- timestamp - The timestamp in which this record was calculated/created.
- The properties
startandtimestampare in UTC you'll need to adjust them to your users time zone. More about UTC in Getting familiar tutorial.
private fun showRecordsInfo(heartRateRecords: List<HeartRateDerivedData>, stepsRecords: List<StepsDerivedData>) {
binding.heartRateRecords.text = heartRateRecords.size.toString()
binding.stepsRecords.text = stepsRecords.size.toString()
}
Running the app
Wrapping up
At the end of this tutorial your app should be able to:
Finish a training and be redirected to the summary screen.
Show training's basic info, summaries and amount of records.

Next steps
Congratulations! You successfully retrieved a summary of a finished training.