image

22 Mar 2026

9K

35K

Building a Habit Tracker Widget with Weekly Progress and Streak Badges in Flutter

Habit trackers are powerful tools for personal development, helping individuals build consistency and achieve long-term goals. In this article, we'll explore how to construct a sophisticated habit tracker widget in Flutter, complete with visual feedback for weekly progress and engaging streak badges. This widget will be designed for reusability and clarity, demonstrating key Flutter concepts for building interactive UI components.

1. The Data Model: Representing Habits and Completions

First, we need a robust data model to represent a habit and its associated completion dates. This model will serve as the single source of truth for our widget's state.


import 'package:flutter/material.dart';

// Helper function to check if two DateTime objects represent the same day
bool isSameDay(DateTime a, DateTime b) {
  return a.year == b.year && a.month == b.month && a.day == b.day;
}

class Habit {
  final String id;
  final String name;
  final List<DateTime> completedDates; // Stores only the date part

  Habit({
    required this.id,
    required this.name,
    List<DateTime>? completedDates,
  }) : completedDates = completedDates ?? [];

  Habit copyWith({
    String? id,
    String? name,
    List<DateTime>? completedDates,
  }) {
    return Habit(
      id: id ?? this.id,
      name: name ?? this.name,
      completedDates: completedDates ?? this.completedDates,
    );
  }

  // Check if the habit was completed on a specific day
  bool isCompletedOn(DateTime date) {
    return completedDates.any((d) => isSameDay(d, date));
  }

  // Toggles completion for a given date
  Habit toggleCompletion(DateTime date) {
    List<DateTime> newCompletedDates = List.from(completedDates);
    if (isCompletedOn(date)) {
      newCompletedDates.removeWhere((d) => isSameDay(d, date));
    } else {
      newCompletedDates.add(date);
    }
    // Sort to ensure consistency, useful for streak calculation later
    newCompletedDates.sort((a, b) => a.compareTo(b));
    return copyWith(completedDates: newCompletedDates);
  }

  // Calculates the current active streak
  int calculateCurrentStreak() {
    List<DateTime> uniqueSortedDates = completedDates
        .map((d) => DateTime(d.year, d.month, d.day))
        .toSet()
        .toList();
    uniqueSortedDates.sort((a, b) => a.compareTo(b));

    if (uniqueSortedDates.isEmpty) return 0;

    DateTime effectiveToday = DateTime.now();
    DateTime effectiveYesterday = effectiveToday.subtract(const Duration(days: 1));

    bool todayCompleted = uniqueSortedDates.any((d) => isSameDay(d, effectiveToday));
    bool yesterdayCompleted = uniqueSortedDates.any((d) => isSameDay(d, effectiveYesterday));

    if (!todayCompleted && !yesterdayCompleted) {
      return 0; // No active streak today or yesterday
    }

    DateTime currentDayBeingChecked = todayCompleted ? effectiveToday : effectiveYesterday;
    int currentStreak = 0;

    while (uniqueSortedDates.any((d) => isSameDay(d, currentDayBeingChecked))) {
      currentStreak++;
      currentDayBeingChecked = currentDayBeingChecked.subtract(const Duration(days: 1));
    }
    return currentStreak;
  }
}

2. Core Widget Structure

Our main HabitTrackerWidget will be a StatelessWidget, receiving the habit data and an onToggle callback from its parent. This promotes a cleaner separation of concerns, where the parent manages state and persistence.


import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; // For date formatting, add intl to pubspec.yaml

// (Import Habit class and isSameDay function from previous section)
// import 'habit_model.dart'; // Assuming Habit class is in a separate file

class HabitTrackerWidget extends StatelessWidget {
  final Habit habit;
  final ValueChanged<Habit> onToggle;

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

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  habit.name,
                  style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
                ),
                StreakBadge(streak: habit.calculateCurrentStreak()),
              ],
            ),
            const SizedBox(height: 10),
            WeeklyProgressRow(
              habit: habit,
              onDayToggled: (date) {
                onToggle(habit.toggleCompletion(date));
              },
            ),
          ],
        ),
      ),
    );
  }
}

3. Implementing Weekly Progress

The WeeklyProgressRow widget displays the days of the current week and indicates whether the habit was completed on each day. It also handles user interaction to mark a day as complete or incomplete.


// (Import Habit class and isSameDay function from previous sections)
// import 'habit_model.dart';

class WeeklyProgressRow extends StatelessWidget {
  final Habit habit;
  final ValueChanged<DateTime> onDayToggled;

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

  @override
  Widget build(BuildContext context) {
    DateTime now = DateTime.now();
    DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1)); // Monday
    
    // Adjust for locales where week starts on Sunday (e.g., U.S.)
    // If you want week to start on Sunday: `now.weekday % 7`
    // For simplicity, let's assume Monday start for this example.

    List<Widget> dayWidgets = [];
    int completedDaysCount = 0;

    for (int i = 0; i < 7; i++) {
      DateTime day = startOfWeek.add(Duration(days: i));
      bool isToday = isSameDay(day, now);
      bool isCompleted = habit.isCompletedOn(day);

      if (isCompleted) {
        completedDaysCount++;
      }

      dayWidgets.add(
        GestureDetector(
          onTap: () {
            // Only allow toggling for today or past days (optional restriction)
            // if (day.isBefore(now.add(const Duration(days: 1)))) {
               onDayToggled(day);
            // }
          },
          child: Container(
            margin: const EdgeInsets.symmetric(horizontal: 2),
            width: 30,
            height: 30,
            decoration: BoxDecoration(
              color: isCompleted
                  ? Colors.green.shade400
                  : (isToday ? Colors.blue.shade100 : Colors.grey.shade300),
              borderRadius: BorderRadius.circular(8),
              border: isToday
                  ? Border.all(color: Colors.blue.shade700, width: 2)
                  : null,
            ),
            child: Center(
              child: Text(
                DateFormat('EE').format(day)[0], // First letter of day name
                style: TextStyle(
                  color: isCompleted || isToday ? Colors.white : Colors.black87,
                  fontWeight: FontWeight.bold,
                  fontSize: 12,
                ),
              ),
            ),
          ),
        ),
      );
    }

    double weeklyProgress = (completedDaysCount / 7) * 100;

    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: dayWidgets,
        ),
        const SizedBox(height: 8),
        LinearProgressIndicator(
          value: weeklyProgress / 100,
          backgroundColor: Colors.grey.shade200,
          color: Colors.green,
          minHeight: 8,
          borderRadius: BorderRadius.circular(4),
        ),
        const SizedBox(height: 4),
        Align(
          alignment: Alignment.centerRight,
          child: Text(
            '${weeklyProgress.toStringAsFixed(0)}% This Week',
            style: const TextStyle(fontSize: 12, color: Colors.grey),
          ),
        ),
      ],
    );
  }
}

4. Displaying Streak Badges

Streak badges provide gamification and a sense of accomplishment. The StreakBadge widget will display the current streak count and optionally show a special icon for milestones.


import 'package:flutter/material.dart';

class StreakBadge extends StatelessWidget {
  final int streak;

  const StreakBadge({
    Key? key,
    required this.streak,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    if (streak == 0) {
      return const SizedBox.shrink(); // Don't show badge if streak is 0
    }

    IconData iconData;
    Color badgeColor;

    if (streak >= 30) {
      iconData = Icons.star;
      badgeColor = Colors.amber;
    } else if (streak >= 7) {
      iconData = Icons.local_fire_department;
      badgeColor = Colors.orange;
    } else {
      iconData = Icons.fiber_manual_record;
      badgeColor = Colors.red;
    }

    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
      decoration: BoxDecoration(
        color: badgeColor.withOpacity(0.2),
        borderRadius: BorderRadius.circular(20),
        border: Border.all(color: badgeColor, width: 1),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(
            iconData,
            color: badgeColor,
            size: 16,
          ),
          const SizedBox(width: 4),
          Text(
            '$streak Day${streak == 1 ? '' : 's'} Streak',
            style: TextStyle(
              color: badgeColor,
              fontWeight: FontWeight.bold,
              fontSize: 12,
            ),
          ),
        ],
      ),
    );
  }
}

5. Putting It All Together: Example Usage

To see our widget in action, we'll create a simple StatefulWidget that holds a list of habits and passes them to our HabitTrackerWidgets. This example uses basic setState for state management, but in a real application, you might use Provider, Riverpod, BLoC, or GetX.


import 'package:flutter/material.dart';
// import 'habit_model.dart'; // Ensure Habit model is imported
// import 'habit_tracker_widget.dart'; // Ensure HabitTrackerWidget is imported
// import 'weekly_progress_row.dart'; // Ensure WeeklyProgressRow is imported
// import 'streak_badge.dart'; // Ensure StreakBadge is imported

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Habit Tracker',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const HabitTrackerScreen(),
    );
  }
}

class HabitTrackerScreen extends StatefulWidget {
  const HabitTrackerScreen({Key? key}) : super(key: key);

  @override
  State<HabitTrackerScreen> createState() => _HabitTrackerScreenState();
}

class _HabitTrackerScreenState extends State<HabitTrackerScreen> {
  List<Habit> _habits = [
    Habit(
      id: 'h1',
      name: 'Drink 8 Glasses of Water',
      completedDates: [
        DateTime.now().subtract(const Duration(days: 3)),
        DateTime.now().subtract(const Duration(days: 2)),
        DateTime.now().subtract(const Duration(days: 1)),
        DateTime.now(), // Completed today
      ],
    ),
    Habit(
      id: 'h2',
      name: 'Read 30 Minutes',
      completedDates: [
        DateTime.now().subtract(const Duration(days: 5)),
        DateTime.now().subtract(const Duration(days: 4)),
        DateTime.now().subtract(const Duration(days: 3)),
      ],
    ),
    Habit(
      id: 'h3',
      name: 'Exercise for 60 Minutes',
      completedDates: [],
    ),
  ];

  void _updateHabit(Habit updatedHabit) {
    setState(() {
      final index = _habits.indexWhere((h) => h.id == updatedHabit.id);
      if (index != -1) {
        _habits[index] = updatedHabit;
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Habits'),
      ),
      body: ListView.builder(
        itemCount: _habits.length,
        itemBuilder: (context, index) {
          final habit = _habits[index];
          return HabitTrackerWidget(
            habit: habit,
            onToggle: _updateHabit,
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // Add functionality to add new habits
          setState(() {
            _habits.add(
              Habit(
                id: 'h${_habits.length + 1}',
                name: 'New Habit ${_habits.length + 1}',
                completedDates: [],
              ),
            );
          });
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Conclusion

We've successfully built a modular and interactive habit tracker widget in Flutter. By separating our logic into a robust data model (Habit), a main widget (HabitTrackerWidget), and specialized sub-widgets (WeeklyProgressRow and StreakBadge), we achieved a clean and maintainable codebase. This architecture allows for easy extension, such as adding different habit types, notifications, or integrating with a persistent storage solution like SQLite or Firebase.

This widget serves as an excellent foundation for any application requiring habit tracking functionality, offering clear visual feedback and gamified elements to encourage user engagement and consistency.

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