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

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

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en