Flutter  

Building A Habit Tracker App In Flutter: Part 4 - Advanced Tracking & Notifications

In Part 3, we implemented the CRUD operations for habits. Now, we will discuss the most intelligent part of our application:Tracking Progress and Reminders.

To keep users motivated, we must accurately track their streaks and provide timely notifications. This involves some complex logic that is very important to get right.

1. The Logic of Streak Calculation

A "Streak" refers to the number of consecutive days a user has completed a habit. Strictly speaking, we need to recalculate this every time a user marks a habit as "Completed".

In our TrackingRepositoryImpl, we handle this logic by comparing the current date with the last completed date.

// tracking_repository_impl.dart
Future<int> _calculateNewStreak(String habitId, DateTime today) async {
  // 1. Fetch the last completion date from database
  final lastDate = await getLastCompletionDate(habitId);
  
  if (lastDate == null) return 1; // First time completion

  // 2. Check the difference in days
  final difference = today.difference(lastDate).inDays;

  if (difference == 1) {
    // Tomorrow from last completion (Consecutive)
    return currentStreak + 1;
  } else if (difference > 1) {
    // Gap found (Streak broken)
    return 1;
  }
  
  return currentStreak; // Already done today
}

This logic ensures that if the user misses even a single day, the streak resets to 1. This gamification is basically why users stay engaged with these types of apps.

2. Managing Habit History

Instead of just updating a counter, we store every single completion as a record in our habit_completions table. This allows us to perform deep analytics later, such as calculating completion rates or showing a heatmap.

-- Database Schema for Completions
CREATE TABLE habit_completions (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  habit_id UUID REFERENCES habits(id),
  user_id UUID REFERENCES auth.users(id),
  completed_date DATE NOT NULL,
  UNIQUE(habit_id, completed_date)
);

By using a Unique Constraint on the habit_id and completed_date, we strictly prevent any duplicate records for the same day.

3. Local Notifications with flutter_local_notifications

To ensure that users don't forget their daily goals, we use local notifications. We don't need a server for this; the phone itself can handle the scheduling.

The NotificationServicehandles the initialization and scheduling:

// core/services/notification_service.dart
Future<void> scheduleHabitReminder(HabitEntity habit) async {
  final reminderTime = habit.reminderTime;
  if (reminderTime == null) return;

  // We use TZDateTime for handling timezones correctly
  final scheduledDate = _nextInstanceOfTime(reminderTime);

  await _notifications.zonedSchedule(
    habit.id.hashCode,
    'Habit Reminder: ${habit.name}',
    'Don\'t forget to stay consistent today! 🔥',
    scheduledDate,
    _notificationDetails(),
    androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
    uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
    matchDateTimeComponents: DateTimeComponents.time,
  );
}

4. Handling Timezones

Timezone handling is a bit tricky but very necessary if your users travel. We use the timezone package to ensure that 9 AM means 9 AM in the user's local time, not according to a static UTC value.

// Initialization
tz.initializeTimeZones();
final String localTimeZone = await FlutterNativeTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(localTimeZone));

Conclusion

We have now added the "brains" to our Habit Tracker. The app can now track user progress accurately and nudge them with notifications when it's time to act. In our final article, we will focus on Data Visualization and UI Polish. We will see how to turn these numbers into beautiful charts and heatmaps. Stay tuned for Part 5!