image

24 Feb 2026

9K

35K

Building a Habit Tracker Widget with Weekly Progress Chart in Flutter

Habit trackers are powerful tools for self-improvement, helping users cultivate positive routines and monitor their progress over time. Developing such a tool in Flutter offers a highly customizable and visually appealing experience. This article will guide you through creating a core habit tracker widget featuring a clear weekly progress chart, demonstrating key Flutter concepts like state management, custom UI, and data modeling.

1. Designing the Habit Data Model

The foundation of any tracker is its data model. We need a way to represent a habit and its completion history. A simple Habit class can store the habit's name, a unique identifier, and a list of DateTime objects indicating when the habit was successfully completed.


import 'package:flutter/material.dart';

class Habit {
  final String id;
  final String name;
  final List<DateTime> completionDates;

  Habit({required this.id, required this.name, required this.completionDates});

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

  // Helper to add or remove a completion date
  Habit toggleCompletion(DateTime date) {
    final updatedCompletionDates = List<DateTime>.from(completionDates);
    if (isCompletedOn(date)) {
      // Remove the completion date if it already exists
      updatedCompletionDates.removeWhere((d) =>
          d.year == date.year && d.month == date.month && d.day == date.day);
    } else {
      // Add the completion date if it doesn't exist
      updatedCompletionDates.add(date);
    }
    // Return a new Habit instance with updated completion dates
    return Habit(
        id: id, name: name, completionDates: updatedCompletionDates);
  }
}

The toggleCompletion method is particularly useful as it allows us to immutably update the habit's state, returning a new Habit object with the modified completionDates list. This approach aligns well with Flutter's state management principles.

2. Building the Habit Tracker Widget

Our habit tracker will be a StatefulWidget, allowing it to manage its internal state, specifically the completion status of the habit for each day. It will also accept a callback to notify a parent widget about changes, facilitating data persistence.


class HabitTrackerWidget extends StatefulWidget {
  final Habit habit;
  final ValueChanged<Habit> onHabitChanged;

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

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

class _HabitTrackerWidgetState extends State<HabitTrackerWidget> {
  // A local copy of the habit to manage UI updates
  late Habit _currentHabit;

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

  // Update local habit if the parent widget provides a new habit instance
  @override
  void didUpdateWidget(covariant HabitTrackerWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.habit != oldWidget.habit) {
      setState(() {
        _currentHabit = widget.habit;
      });
    }
  }

  // Toggles the completion status for a given day
  void _toggleDayCompletion(DateTime date) {
    setState(() {
      _currentHabit = _currentHabit.toggleCompletion(date);
    });
    // Notify the parent widget about the change for potential persistence
    widget.onHabitChanged(_currentHabit);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16.0),
      margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.0),
        boxShadow: [
          BoxShadow(
            color: Colors.grey.withOpacity(0.15),
            spreadRadius: 1,
            blurRadius: 5,
            offset: const Offset(0, 3), // changes position of shadow
          ),
        ],
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            _currentHabit.name,
            style: const TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.bold,
              color: Colors.deepPurple,
            ),
          ),
          const SizedBox(height: 15),
          _buildWeeklyProgressChart(), // The chart goes here
        ],
      ),
    );
  }

  // Builds the row of 7 day cells for the weekly progress chart
  Widget _buildWeeklyProgressChart() {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: List.generate(7, (index) {
        final now = DateTime.now();
        // Calculate the date for each day cell (last 7 days including today)
        final day = DateTime(now.year, now.month, now.day)
            .subtract(Duration(days: 6 - index));
        return _buildDayCell(day);
      }),
    );
  }

  // Builds an individual day cell
  Widget _buildDayCell(DateTime day) {
    final bool isCompleted = _currentHabit.isCompletedOn(day);
    final bool isToday = day.year == DateTime.now().year &&
        day.month == DateTime.now().month &&
        day.day == DateTime.now().day;

    return GestureDetector(
      onTap: () => _toggleDayCompletion(day),
      child: Container(
        width: 40,
        height: 40,
        decoration: BoxDecoration(
          color: isCompleted ? Colors.deepPurpleAccent : Colors.grey[200],
          shape: BoxShape.circle,
          border: isToday
              ? Border.all(color: Colors.deepPurple, width: 2.5)
              : null,
        ),
        child: Center(
          child: Text(
            _getWeekdayShortName(day.weekday),
            style: TextStyle(
              color: isCompleted ? Colors.white : Colors.grey[700],
              fontWeight: FontWeight.bold,
              fontSize: 13,
            ),
          ),
        ),
      ),
    );
  }

  // Helper to get short weekday names
  String _getWeekdayShortName(int weekday) {
    switch (weekday) {
      case DateTime.monday:
        return 'Mon';
      case DateTime.tuesday:
        return 'Tue';
      case DateTime.wednesday:
        return 'Wed';
      case DateTime.thursday:
        return 'Thu';
      case DateTime.friday:
        return 'Fri';
      case DateTime.saturday:
        return 'Sat';
      case DateTime.sunday:
        return 'Sun';
      default:
        return '';
    }
  }
}

Explanation of Key Components:

  • _HabitTrackerWidgetState: Manages the mutable state of the widget, including the current Habit object.
  • _toggleDayCompletion(DateTime date): This method updates the habit's completion status for a specific day. It uses setState to trigger a UI rebuild and calls widget.onHabitChanged to propagate the change to its parent.
  • _buildWeeklyProgressChart(): This private method generates a Row containing seven individual day cells. It calculates the last seven days, including the current day, to display in the chart.
  • _buildDayCell(DateTime day): This is a GestureDetector wrapped around a Container. It visually represents a single day:
    • Its background color changes based on whether the habit was completed on that day (_currentHabit.isCompletedOn(day)).
    • A distinct border highlights the current day.
    • Tapping the cell invokes _toggleDayCompletion, allowing users to mark or unmark a day.
  • _getWeekdayShortName(int weekday): A utility function to convert integer weekday values (1 for Monday, 7 for Sunday) into readable short names.

3. Integrating the Widget into an Application

To demonstrate the HabitTrackerWidget, let's embed it within a simple Flutter application. The parent widget (e.g., HabitListScreen) will hold a list of habits and manage their updates, simulating data persistence.


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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Habit Tracker Demo',
      theme: ThemeData(
        primarySwatch: Colors.deepPurple,
        scaffoldBackgroundColor: Colors.deepPurple[50],
        appBarTheme: const AppBarTheme(
          backgroundColor: Colors.deepPurple,
          foregroundColor: Colors.white,
        ),
      ),
      home: const HabitListScreen(),
    );
  }
}

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

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

class _HabitListScreenState extends State<HabitListScreen> {
  // Initial dummy habits for demonstration
  List<Habit> _habits = [
    Habit(
      id: '1',
      name: 'Drink 8 Glasses of Water',
      completionDates: [
        DateTime.now().subtract(const Duration(days: 1)), // Yesterday
        DateTime.now().subtract(const Duration(days: 3)),
        DateTime.now(), // Today
      ],
    ),
    Habit(
      id: '2',
      name: 'Exercise for 30 Minutes',
      completionDates: [
        DateTime.now().subtract(const Duration(days: 0)), // Today
        DateTime.now().subtract(const Duration(days: 2)),
      ],
    ),
    Habit(
      id: '3',
      name: 'Read a Book',
      completionDates: [
        DateTime.now().subtract(const Duration(days: 4)),
      ],
    ),
  ];

  // Callback function to update a habit in the list
  void _updateHabit(Habit updatedHabit) {
    setState(() {
      _habits = _habits.map((habit) =>
          habit.id == updatedHabit.id ? updatedHabit : habit).toList();
      // In a real application, you would typically save `updatedHabit`
      // to a persistent storage (e.g., SQLite, Firebase, SharedPreferences).
    });
    // Optional: Show a confirmation message
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('"${updatedHabit.name}" updated!')),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Habits'),
        centerTitle: true,
      ),
      body: ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemCount: _habits.length,
        itemBuilder: (context, index) {
          final habit = _habits[index];
          return HabitTrackerWidget(
            habit: habit,
            onHabitChanged: _updateHabit,
          );
        },
      ),
    );
  }
}

In this example, HabitListScreen maintains a list of Habit objects. When HabitTrackerWidget calls onHabitChanged, the _updateHabit method in HabitListScreen receives the modified habit and updates its internal list, triggering a rebuild of the UI to reflect the changes. This callback mechanism is where you would integrate your actual data persistence logic.

Conclusion

By combining a clear data model with Flutter's declarative UI, we've successfully created an interactive habit tracker widget complete with a weekly progress chart. This foundational component can be further enhanced with features like custom date ranges, habit categories, streak tracking, more sophisticated animations, and robust data persistence. Flutter's flexibility and rich widget set make it an excellent choice for building engaging and functional productivity tools.

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