While modern Android development often leverages Jetpack Compose for declarative UI design, many existing and simpler applications still rely on XML-based layouts. This post explores how to build fitness tracking apps using XML UI in Android, highlighting best practices, challenges, and example code snippets.
Why Use XML for UI Development?
Despite the rise of Jetpack Compose, XML-based layouts continue to be relevant due to:
- Legacy Projects: Many existing Android applications are built using XML and require ongoing maintenance and updates.
- Simplicity for Basic Layouts: XML can be straightforward for creating simple, static layouts.
- Tooling Support: Android Studio provides excellent tooling support for XML, including a visual layout editor.
Overview of Building a Fitness Tracking App with XML
Building a fitness tracking app with XML involves several key components:
- User Interface Design: Creating XML layouts for various screens such as activity tracking, progress charts, user profiles, and settings.
- Data Storage: Implementing local storage (e.g., SQLite) to persist user data such as activity logs, steps, distance, and calories burned.
- Background Services: Using services to track fitness activities in the background.
- Data Visualization: Displaying tracking data through charts and graphs using libraries like MPAndroidChart.
Setting Up the Project
Start by creating a new Android project in Android Studio, selecting the “Empty Activity” template.
Adding Dependencies
Add necessary dependencies to your build.gradle
file. For example:
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.room:room-runtime:2.6.1'
annotationProcessor 'androidx.room:room-compiler:2.6.1'
kapt 'androidx.room:room-compiler:2.6.1'
// Chart Library
implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
Sync the Gradle files after adding the dependencies.
Designing UI Layouts with XML
Create XML layouts for the main screens of the fitness tracking app.
Main Activity Layout (activity_main.xml
)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/stepsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Steps: 0"
android:textSize="20sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp"/>
<Button
android:id="@+id/startButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Tracking"
app:layout_constraintTop_toBottomOf="@+id/stepsTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<com.github.mikephil.charting.charts.LineChart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginTop="32dp"
app:layout_constraintTop_toBottomOf="@+id/startButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Activity Tracking Layout (activity_tracking.xml
)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".TrackingActivity">
<TextView
android:id="@+id/distanceTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Distance: 0 meters"
android:textSize="18sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
<TextView
android:id="@+id/caloriesTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Calories Burned: 0 kcal"
android:textSize="18sp"
app:layout_constraintTop_toBottomOf="@+id/distanceTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="8dp"/>
<Button
android:id="@+id/stopButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Stop Tracking"
app:layout_constraintTop_toBottomOf="@+id/caloriesTextView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>
Implementing Data Storage with SQLite
Use Android’s Room Persistence Library for local data storage. Define entities, DAOs (Data Access Objects), and the database.
Entity (ActivityLog.java
)
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "activity_logs")
public class ActivityLog {
@PrimaryKey(autoGenerate = true)
private int id;
private long timestamp;
private int steps;
private double distance;
private double calories;
// Getters and setters
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public int getSteps() {
return steps;
}
public void setSteps(int steps) {
this.steps = steps;
}
public double getDistance() {
return distance;
}
public void setDistance(double distance) {
this.distance = distance;
}
public double getCalories() {
return calories;
}
public void setCalories(double calories) {
this.calories = calories;
}
}
DAO (ActivityLogDao.java
)
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface ActivityLogDao {
@Insert
void insert(ActivityLog activityLog);
@Query("SELECT * FROM activity_logs ORDER BY timestamp DESC")
List<ActivityLog> getAllActivityLogs();
}
Database (AppDatabase.java
)
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {ActivityLog.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract ActivityLogDao activityLogDao();
}
Initialize the Database
import androidx.room.Room;
// In your Application class or MainActivity
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "fitness-db").build();
Tracking Fitness Activities with Background Services
Create a service to track fitness data in the background using the Sensor API.
Tracking Service (TrackingService.java
)
import android.app.Service;
import android.content.Intent;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.IBinder;
import androidx.annotation.Nullable;
public class TrackingService extends Service implements SensorEventListener {
private SensorManager sensorManager;
private Sensor stepSensor;
private int stepCount = 0;
@Override
public void onCreate() {
super.onCreate();
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
if (sensorManager != null) {
stepSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER);
if (stepSensor != null) {
sensorManager.registerListener(this, stepSensor, SensorManager.SENSOR_DELAY_NORMAL);
}
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Start tracking
return START_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
if (sensorManager != null) {
sensorManager.unregisterListener(this);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_STEP_COUNTER) {
stepCount = (int) event.values[0];
// Store and update step count
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Handle accuracy changes
}
}
Register the service in the AndroidManifest.xml
:
<service android:name=".TrackingService"
android:enabled="true"
android:exported="false"/>
Visualizing Data with MPAndroidChart
Integrate the MPAndroidChart library to display fitness data in charts and graphs.
Populating the Chart (in MainActivity.java
)
import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private LineChart chart;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
chart = findViewById(R.id.chart);
List<Entry> entries = new ArrayList<>();
// Populate entries with data from your database
// Example:
entries.add(new Entry(1, 10));
entries.add(new Entry(2, 15));
entries.add(new Entry(3, 13));
LineDataSet dataSet = new LineDataSet(entries, "Steps");
LineData lineData = new LineData(dataSet);
chart.setData(lineData);
chart.invalidate(); // Refresh chart
}
}
User Profile and Settings
Design XML layouts for user profile management and app settings. Use SharedPreferences to store user preferences.
Best Practices for XML-Based Fitness App Development
- Clean and Organized XML: Use comments and proper indentation for readability.
- Resource Management: Optimize images and use vector drawables to reduce app size.
- Accessibility: Provide content descriptions for UI elements to improve accessibility.
- Performance Optimization: Avoid deep layout hierarchies and use ConstraintLayout effectively to reduce view inflation time.
Challenges and Considerations
- Background Processing: Managing background services efficiently to minimize battery drain.
- Data Accuracy: Ensuring accuracy of sensor data and handling potential errors.
- UI Responsiveness: Maintaining a smooth and responsive UI, especially when dealing with large datasets and charts.
Conclusion
Building a fitness tracking app using XML UI in Android involves careful UI design, efficient data storage, background processing, and data visualization. While modern approaches like Jetpack Compose offer advantages, understanding XML-based layouts remains essential for maintaining legacy projects and building simpler applications. By following best practices and addressing common challenges, developers can create functional and engaging fitness tracking apps using XML UI.