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!