Jetpack Compose: Building Fitness Tracker Apps

Jetpack Compose is Android’s modern toolkit for building native UI. It simplifies and accelerates UI development with less code, powerful tools, and intuitive Kotlin APIs. Creating a fitness tracker app is a great way to explore Compose’s capabilities. This post provides a comprehensive guide on building a fitness tracker app using Jetpack Compose.

What is Jetpack Compose?

Jetpack Compose is a declarative UI framework, allowing you to describe the UI by composing smaller, reusable composable functions. When the app’s state changes, the UI automatically updates. This approach simplifies UI development compared to traditional XML-based layouts.

Why Use Jetpack Compose for a Fitness Tracker App?

  • Simplified UI Development: Compose’s declarative nature reduces boilerplate code.
  • Dynamic UI Updates: React to changes in fitness data in real time.
  • Customization: Easily create custom UI components to match your app’s branding.
  • Kotlin Integration: Seamlessly integrates with Kotlin coroutines and other modern Android technologies.

Setting Up Your Project

First, create a new Android project with Jetpack Compose support in Android Studio. Ensure your build.gradle file has Compose dependencies configured.


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(platform("androidx.compose:compose-bom:2023.03.00"))
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")

    // Navigation Compose
    implementation("androidx.navigation:navigation-compose:2.5.3")

    // Hilt Dependency Injection
    implementation("com.google.dagger:hilt-android:2.44")
    kapt("com.google.dagger:hilt-android-compiler:2.44")
    implementation("androidx.hilt:hilt-navigation-compose:1.0.0")

    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")

    // Room Database
    implementation("androidx.room:room-runtime:2.5.1")
    kapt("androidx.room:room-compiler:2.5.1")
    implementation("androidx.room:room-ktx:2.5.1")

    // Preferences DataStore
    implementation("androidx.datastore:datastore-preferences:1.0.0")
}

kapt {
    correctErrorTypes = true
}

UI Components for a Fitness Tracker App

Here are the key UI components needed for a basic fitness tracker app:

  • Dashboard: Displays daily activity summaries (steps, distance, calories).
  • Activity Log: Shows historical fitness data (workouts, step counts).
  • Settings: Allows users to configure profiles, goals, and preferences.

Implementing UI Components in Jetpack Compose

Let’s walk through implementing some of these components using Jetpack Compose.

Dashboard UI

The dashboard composable displays the daily fitness summary.


import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun Dashboard(steps: Int, distance: Double, calories: Int) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Daily Summary", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))

        StatisticCard(title = "Steps", value = steps.toString())
        Spacer(modifier = Modifier.height(8.dp))

        StatisticCard(title = "Distance", value = String.format("%.2f km", distance))
        Spacer(modifier = Modifier.height(8.dp))

        StatisticCard(title = "Calories Burned", value = calories.toString())
    }
}

@Composable
fun StatisticCard(title: String, value: String) {
    Card(modifier = Modifier.fillMaxWidth()) {
        Column(
            modifier = Modifier.padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Text(text = title, style = MaterialTheme.typography.titleMedium)
            Text(text = value, style = MaterialTheme.typography.bodyLarge)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun DashboardPreview() {
    Dashboard(steps = 5240, distance = 3.75, calories = 280)
}

Explanation:

  • Dashboard: The main composable for displaying fitness summaries.
  • StatisticCard: A reusable composable for displaying individual fitness stats.
  • Modifier.padding: Adds padding around the elements for a better look.
  • Text: Displays the textual information with predefined styles from MaterialTheme.

Activity Log UI

The activity log displays a list of historical fitness data.


import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

data class Activity(val name: String, val duration: String, val calories: Int)

@Composable
fun ActivityLog(activities: List) {
    LazyColumn(
        modifier = Modifier.fillMaxSize()
    ) {
        items(activities) { activity ->
            ActivityCard(activity = activity)
        }
    }
}

@Composable
fun ActivityCard(activity: Activity) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(8.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = activity.name, style = MaterialTheme.typography.titleMedium)
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = "Duration: ${activity.duration}", style = MaterialTheme.typography.bodyMedium)
            Text(text = "Calories Burned: ${activity.calories}", style = MaterialTheme.typography.bodyMedium)
        }
    }
}

@Preview(showBackground = true)
@Composable
fun ActivityLogPreview() {
    val sampleActivities = listOf(
        Activity("Running", "30 minutes", 350),
        Activity("Walking", "60 minutes", 200),
        Activity("Cycling", "45 minutes", 400)
    )
    ActivityLog(activities = sampleActivities)
}

Explanation:

  • ActivityLog: Composable that displays a list of activities using LazyColumn for efficient scrolling.
  • ActivityCard: Represents each activity in a card format.
  • items(activities): Iterates through the list of activities and displays each one.

Settings UI

The settings composable allows users to configure app preferences and profiles.


import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun SettingsScreen() {
    var weight by remember { mutableStateOf("70 kg") }
    var height by remember { mutableStateOf("175 cm") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(text = "Profile Settings", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(16.dp))

        OutlinedTextField(
            value = weight,
            onValueChange = { weight = it },
            label = { Text("Weight") },
            modifier = Modifier.fillMaxWidth()
        )
        Spacer(modifier = Modifier.height(8.dp))

        OutlinedTextField(
            value = height,
            onValueChange = { height = it },
            label = { Text("Height") },
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { /*TODO: Save settings*/ }, modifier = Modifier.fillMaxWidth()) {
            Text(text = "Save")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun SettingsScreenPreview() {
    SettingsScreen()
}

Explanation:

  • SettingsScreen: The composable that displays profile settings, such as weight and height.
  • OutlinedTextField: Used for accepting user input.
  • remember { mutableStateOf() }: Used to remember and update the state of the input fields.

Implementing Navigation with Jetpack Compose

For more complex applications, using a navigation component is crucial.


import androidx.compose.runtime.Composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable

@Composable
fun Navigation() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "dashboard") {
        composable("dashboard") {
            Dashboard(steps = 5240, distance = 3.75, calories = 280)
        }
        composable("activityLog") {
            val sampleActivities = listOf(
                Activity("Running", "30 minutes", 350),
                Activity("Walking", "60 minutes", 200),
                Activity("Cycling", "45 minutes", 400)
            )
            ActivityLog(activities = sampleActivities)
        }
        composable("settings") {
            SettingsScreen()
        }
    }
}

Connecting Data with State Management

Data needs to be managed effectively. ViewModel and State Hoisting can be used together.


import androidx.lifecycle.ViewModel
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.State
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class FitnessViewModel : ViewModel() {

    private val _steps = mutableStateOf(0)
    val steps: State = _steps

    fun incrementSteps(newSteps: Int) {
        viewModelScope.launch {
            _steps.value = newSteps
        }
    }
}

Conclusion

Using Jetpack Compose simplifies the creation of a fitness tracker app. Its declarative syntax and powerful tools make it easier to create interactive UIs. Implementing features such as dashboard displays, activity logs, and settings screens becomes more streamlined. By connecting with data through state management, the application is able to reactively represent the most current information to the user.