image

13 Mar 2026

9K

35K

Building a Habit Tracker Widget with Weekly Progress, Streaks, and Notifications in Flutter

Habit tracking applications are powerful tools for personal development, helping users build consistency and achieve their goals. Creating a custom habit tracker widget in Flutter allows for a highly personalized and engaging user experience. This article will guide you through the process of building such a widget, incorporating key features like weekly progress visualization, streak calculation, and local notifications.

Understanding the Core Components

Before diving into the code, let's outline the fundamental components required for our habit tracker:

  • Data Model: To store habit details and their completion status.
  • Weekly Progress: A visual representation of a habit's completion over the past seven days.
  • Streaks: Logic to calculate and display consecutive days a habit has been completed.
  • Notifications: Scheduled reminders to prompt the user to complete their habits.
  • State Management: How the widget will react to user interactions and data changes.

1. Data Model for Habits

We need a robust data model to represent a habit. This model should include basic information like the habit's name, a unique identifier, and most importantly, a list of dates when the habit was completed. For simplicity, we'll store dates as DateTime objects.


import 'package:flutter/material.dart';

class Habit {
  final String id;
  String name;
  String description;
  List<DateTime> completedDates;
  TimeOfDay? reminderTime;

  Habit({
    required this.id,
    required this.name,
    this.description = '',
    this.completedDates = const [],
    this.reminderTime,
  });

  // Helper to check if a habit was completed on a specific day
  bool isCompletedOn(DateTime date) {
    return completedDates.any(
      (d) => d.year == date.year && d.month == date.month && d.day == date.day,
    );
  }

  // Add completion for today
  void toggleCompletionForDate(DateTime date, {required bool isCompleted}) {
    // Normalize date to remove time component for comparison
    final normalizedDate = DateTime(date.year, date.month, date.day);

    if (isCompleted) {
      if (!isCompletedOn(normalizedDate)) {
        completedDates = List.from(completedDates)..add(normalizedDate);
      }
    } else {
      completedDates = completedDates.where(
        (d) => !(d.year == normalizedDate.year && d.month == normalizedDate.month && d.day == normalizedDate.day)
      ).toList();
    }
    // Optionally sort completedDates for streak calculation efficiency
    completedDates.sort((a, b) => a.compareTo(b));
  }

  // Convert Habit to JSON for persistence
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'description': description,
      'completedDates': completedDates.map((date) => date.toIso8601String()).toList(),
      'reminderTimeHour': reminderTime?.hour,
      'reminderTimeMinute': reminderTime?.minute,
    };
  }

  // Create Habit from JSON
  factory Habit.fromJson(Map<String, dynamic> json) {
    return Habit(
      id: json['id'],
      name: json['name'],
      description: json['description'] ?? '',
      completedDates: (json['completedDates'] as List<dynamic>?)
          ?.map((dateString) => DateTime.parse(dateString))
          .toList() ?? [],
      reminderTime: json['reminderTimeHour'] != null && json['reminderTimeMinute'] != null
          ? TimeOfDay(hour: json['reminderTimeHour'], minute: json['reminderTimeMinute'])
          : null,
    );
  }
}

2. Implementing Weekly Progress Display

The weekly progress display will show the user a visual overview of their habit completion for the last seven days. This typically involves a row of clickable indicators (e.g., circles or squares), where each indicator represents a day.


import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // For date formatting

class WeeklyProgressIndicator extends StatelessWidget {
  final Habit habit;
  final Function(DateTime date, bool isCompleted) onToggleCompletion;

  const WeeklyProgressIndicator({
    Key? key,
    required this.habit,
    required this.onToggleCompletion,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    List<Widget> dayIndicators = [];
    DateTime now = DateTime.now();

    for (int i = 6; i >= 0; i--) { // Iterate for the last 7 days including today
      DateTime day = DateTime(now.year, now.month, now.day).subtract(Duration(days: i));
      bool isCompleted = habit.isCompletedOn(day);

      dayIndicators.add(
        GestureDetector(
          onTap: () {
            onToggleCompletion(day, !isCompleted);
          },
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 4.0),
            child: Column(
              children: [
                Text(
                  i == 0 ? 'Today' : DateFormat('EEE').format(day), // Show 'Today' or day of week
                  style: Theme.of(context).textTheme.bodySmall,
                ),
                SizedBox(height: 4),
                Container(
                  width: 30,
                  height: 30,
                  decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    color: isCompleted ? Colors.green : Colors.grey[300],
                    border: Border.all(
                      color: isCompleted ? Colors.greenAccent : Colors.grey,
                      width: 1.5,
                    ),
                  ),
                  child: isCompleted
                      ? Icon(Icons.check, size: 18, color: Colors.white)
                      : Container(),
                ),
              ],
            ),
          ),
        ),
      );
    }

    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceAround,
      children: dayIndicators.reversed.toList(), // Display from past to today
    );
  }
}

3. Calculating Streaks

Streaks encourage users by showing them their consistent effort. A "current streak" tracks consecutive completion up to today or yesterday. A "longest streak" records the maximum consecutive days achieved.


// Inside the Habit class or a dedicated utility class
extension HabitStreaks on Habit {
  int calculateCurrentStreak() {
    if (completedDates.isEmpty) return 0;

    int streak = 0;
    // Sort for consistent streak calculation
    List<DateTime> sortedCompletedDates = List.from(completedDates);
    sortedCompletedDates.sort((a, b) => a.compareTo(b));

    DateTime now = DateTime.now();
    DateTime today = DateTime(now.year, now.month, now.day);
    DateTime yesterday = today.subtract(const Duration(days: 1));

    // Check if habit was completed today or yesterday to start the streak
    bool completedToday = isCompletedOn(today);
    bool completedYesterday = isCompletedOn(yesterday);

    if (!completedToday && !completedYesterday) return 0; // Streak broken

    // Start checking from the most recent date
    for (int i = sortedCompletedDates.length - 1; i >= 0; i--) {
      DateTime currentCompletion = sortedCompletedDates[i];
      DateTime normalizedCompletion = DateTime(currentCompletion.year, currentCompletion.month, currentCompletion.day);

      if (streak == 0) { // First iteration
        if (normalizedCompletion == today) {
          streak = 1;
        } else if (normalizedCompletion == yesterday && !completedToday) {
          // If today wasn't completed but yesterday was, the streak still counts up to yesterday
          streak = 1;
        } else {
          // If the most recent completion is neither today nor yesterday, no current streak.
          return 0;
        }
      } else {
        DateTime expectedPreviousDay = DateTime(sortedCompletedDates[i+1].year, sortedCompletedDates[i+1].month, sortedCompletedDates[i+1].day).subtract(const Duration(days: 1));
        if (normalizedCompletion == expectedPreviousDay) {
          streak++;
        } else {
          // Gap found, streak broken
          break;
        }
      }
    }
    return streak;
  }

  int calculateLongestStreak() {
    if (completedDates.isEmpty) return 0;

    int longestStreak = 0;
    int currentConsecutive = 0;
    List<DateTime> sortedCompletedDates = List.from(completedDates);
    sortedCompletedDates.sort((a, b) => a.compareTo(b));

    if (sortedCompletedDates.isNotEmpty) {
      currentConsecutive = 1; // Start with the first date
      longestStreak = 1;

      for (int i = 1; i < sortedCompletedDates.length; i++) {
        DateTime prevDay = DateTime(sortedCompletedDates[i-1].year, sortedCompletedDates[i-1].month, sortedCompletedDates[i-1].day);
        DateTime currDay = DateTime(sortedCompletedDates[i].year, sortedCompletedDates[i].month, sortedCompletedDates[i].day);

        // Check if the current day is exactly one day after the previous day
        if (currDay.difference(prevDay).inDays == 1) {
          currentConsecutive++;
        } else {
          currentConsecutive = 1; // Reset streak
        }
        if (currentConsecutive > longestStreak) {
          longestStreak = currentConsecutive;
        }
      }
    }
    return longestStreak;
  }
}

4. Implementing Local Notifications

Flutter's flutter_local_notifications package is ideal for scheduling local notifications. Users can set a specific time for their habit reminders.


import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;

class NotificationService {
  static final NotificationService _notificationService = NotificationService._internal();

  factory NotificationService() {
    return _notificationService;
  }

  NotificationService._internal();

  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

  Future<void> init() async {
    tz.initializeTimeZones(); // Initialize timezone data

    const AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');

    const IOSInitializationSettings initializationSettingsIOS =
        IOSInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );

    const InitializationSettings initializationSettings =
        InitializationSettings(
            android: initializationSettingsAndroid, iOS: initializationSettingsIOS);

    await flutterLocalNotificationsPlugin.initialize(initializationSettings);
  }

  Future<void> showNotification({
    required int id,
    required String title,
    required String body,
    required TimeOfDay scheduledTime,
  }) async {
    // Schedule daily notification
    await flutterLocalNotificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      _nextInstanceOfTime(scheduledTime),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'habit_tracker_channel',
          'Habit Reminders',
          channelDescription: 'Reminders for your daily habits',
          importance: Importance.high,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: IOSNotificationDetails(),
      ),
      androidAllowWhileIdle: true,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the specified time
    );
  }

  tz.TZDateTime _nextInstanceOfTime(TimeOfDay time) {
    final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
    tz.TZDateTime scheduledDate = tz.TZDateTime(
        tz.local, now.year, now.month, now.day, time.hour, time.minute);

    if (scheduledDate.isBefore(now)) {
      scheduledDate = scheduledDate.add(const Duration(days: 1));
    }
    return scheduledDate;
  }

  Future<void> cancelNotification(int id) async {
    await flutterLocalNotificationsPlugin.cancel(id);
  }
}

Remember to add flutter_local_notifications and timezone to your pubspec.yaml.


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^9.x.x
  timezone: ^0.8.0
  intl: ^0.18.0 # For date formatting in WeeklyProgressIndicator

Also, ensure you configure necessary permissions and settings for Android and iOS. For Android, you might need to update your AndroidManifest.xml. For iOS, requesting permissions is handled by the plugin.

5. Putting It All Together: The Habit Widget

Now, let's combine these elements into a single, interactive habit widget. This will typically be a StatefulWidget to manage the habit's completion state and trigger updates.


import 'package:flutter/material.dart';
import 'package:uuid/uuid.dart'; // For unique IDs

// Assuming Habit, WeeklyProgressIndicator, HabitStreaks, NotificationService are defined above.

class HabitTrackerWidget extends StatefulWidget {
  final Habit habit;
  final Function(Habit updatedHabit) onHabitUpdated;

  const HabitTrackerWidget({
    Key? key,
    required this.habit,
    required this.onHabitUpdated,
  }) : super(key: key);

  @override
  _HabitTrackerWidgetState createState() => _HabitTrackerWidgetState();
}

class _HabitTrackerWidgetState extends State<HabitTrackerWidget> {
  late Habit _currentHabit;
  final NotificationService _notificationService = NotificationService();

  @override
  void initState() {
    super.initState();
    _currentHabit = widget.habit;
    _scheduleReminderIfNeeded();
  }

  void _scheduleReminderIfNeeded() {
    if (_currentHabit.reminderTime != null) {
      _notificationService.showNotification(
        id: int.parse(_currentHabit.id.replaceAll(RegExp('[^0-9]'), '')).abs() % 2147483647, // Generate int from String ID
        title: 'Habit Reminder',
        body: 'Time to ${_currentHabit.name}!',
        scheduledTime: _currentHabit.reminderTime!,
      );
    } else {
      // If reminder time is removed, cancel existing notification
      _notificationService.cancelNotification(int.parse(_currentHabit.id.replaceAll(RegExp('[^0-9]'), '')).abs() % 2147483647);
    }
  }

  void _handleToggleCompletion(DateTime date, bool isCompleted) {
    setState(() {
      _currentHabit.toggleCompletionForDate(date, isCompleted: isCompleted);
      widget.onHabitUpdated(_currentHabit); // Notify parent of the change
    });
  }

  Future<void> _pickReminderTime() async {
    final TimeOfDay? picked = await showTimePicker(
      context: context,
      initialTime: _currentHabit.reminderTime ?? TimeOfDay.now(),
    );
    if (picked != null && picked != _currentHabit.reminderTime) {
      setState(() {
        _currentHabit.reminderTime = picked;
        widget.onHabitUpdated(_currentHabit);
        _scheduleReminderIfNeeded();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    final currentStreak = _currentHabit.calculateCurrentStreak();
    final longestStreak = _currentHabit.calculateLongestStreak();

    return Card(
      margin: const EdgeInsets.all(8.0),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              _currentHabit.name,
              style: Theme.of(context).textTheme.headline6,
            ),
            const SizedBox(height: 8),
            Text(
              _currentHabit.description,
              style: Theme.of(context).textTheme.bodyText2,
            ),
            const Divider(),
            WeeklyProgressIndicator(
              habit: _currentHabit,
              onToggleCompletion: _handleToggleCompletion,
            ),
            const Divider(),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceAround,
              children: [
                Column(
                  children: [
                    Text('Current Streak', style: Theme.of(context).textTheme.bodySmall),
                    Text('$currentStreak days', style: Theme.of(context).textTheme.titleLarge),
                  ],
                ),
                Column(
                  children: [
                    Text('Longest Streak', style: Theme.of(context).textTheme.bodySmall),
                    Text('$longestStreak days', style: Theme.of(context).textTheme.titleLarge),
                  ],
                ),
              ],
            ),
            const SizedBox(height: 16),
            Align(
              alignment: Alignment.centerRight,
              child: ElevatedButton.icon(
                onPressed: _pickReminderTime,
                icon: Icon(_currentHabit.reminderTime == null ? Icons.notifications_off : Icons.notifications_active),
                label: Text(
                  _currentHabit.reminderTime == null
                      ? 'Set Reminder'
                      : 'Reminder at ${_currentHabit.reminderTime!.format(context)}',
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

This HabitTrackerWidget takes a Habit object and a callback onHabitUpdated to communicate changes back to its parent (e.g., a list of habits manager). It orchestrates the weekly progress display, streak calculations, and reminder settings. For generating unique IDs, consider using the uuid package.

Conclusion

Building a habit tracker widget in Flutter involves careful data modeling, intuitive UI design for weekly progress, robust logic for streak calculation, and reliable local notification scheduling. By combining these elements, you can create an engaging and functional tool to help users foster positive habits. This article provides a foundational structure; further enhancements could include data persistence (e.g., using Shared Preferences or a local database like Hive/SQLite), advanced statistics, and more customizable UI themes.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is