image

07 Feb 2026

9K

35K

Building a Habit Tracker Widget with Streak Counter in Flutter

Habit trackers are powerful tools that help individuals cultivate positive habits and achieve personal growth. By providing visual feedback on consistency, they motivate users to maintain their routines. Building a habit tracker in Flutter allows for a beautiful, cross-platform application with a native feel. This article will guide you through creating a simple habit tracker widget with a streak counter, highlighting the core logic and Flutter implementation.

Understanding the Core Logic

At the heart of any habit tracker is the ability to record daily completion and calculate streaks. A "streak" represents the number of consecutive days a habit has been performed. This requires tracking several key pieces of information for each habit:

  • Name: A descriptive name for the habit (e.g., "Drink 2 Liters of Water").
  • Last Completed Date: The most recent date the habit was marked as done.
  • Current Streak: The number of consecutive days the habit has been completed.
  • Creation Date: When the habit was initially added.

The Habit Data Model

First, let's define our data model for a single habit. We'll also include a utility extension for easy date comparisons.


// lib/utils/date_time_extension.dart
extension DateTimeExtension on DateTime {
  bool isSameDay(DateTime other) {
    return year == other.year && month == other.month && day == other.day;
  }

  bool isYesterday(DateTime other) {
    final yesterday = other.subtract(const Duration(days: 1));
    return year == yesterday.year && month == yesterday.month && day == yesterday.day;
  }
}

// lib/models/habit.dart
import 'package:flutter/foundation.dart';
import '../utils/date_time_extension.dart';

class Habit {
  final String id;
  String name;
  DateTime? lastCompletedDate;
  int _currentStreak; // Internal streak count
  final DateTime creationDate;

  Habit({
    required this.id,
    required this.name,
    this.lastCompletedDate,
    int initialStreak = 0,
    required this.creationDate,
  }) : _currentStreak = initialStreak;

  // Calculates the effective streak for display, considering missed days.
  int get effectiveStreak {
    if (lastCompletedDate == null) return 0;

    final now = DateTime.now();
    // If completed today, the streak is as recorded.
    if (lastCompletedDate!.isSameDay(now)) {
      return _currentStreak;
    }
    // If completed yesterday, it's still current, but needs to be completed today to increment.
    // Display the recorded streak for continuity.
    if (lastCompletedDate!.isYesterday(now)) {
      return _currentStreak;
    }
    
    // Otherwise, it was missed, so the streak resets to 0 for display.
    return 0;
  }

  // Marks the habit as done for today and updates the internal streak count.
  void markDone() {
    final now = DateTime.now();
    if (lastCompletedDate != null && lastCompletedDate!.isSameDay(now)) {
      // Already done today, no change.
      return;
    }

    if (lastCompletedDate != null && lastCompletedDate!.isYesterday(now)) {
      // Habit was done yesterday, so increment streak.
      _currentStreak++;
    } else {
      // Habit was not done yesterday (or never done before), start a new streak.
      _currentStreak = 1;
    }
    lastCompletedDate = now;
  }

  // Checks if the habit is marked as done for the current day.
  bool get isDoneForToday {
    if (lastCompletedDate == null) return false;
    return lastCompletedDate!.isSameDay(DateTime.now());
  }

  // A convenience method to reset streak explicitly, if needed (e.g., for editing).
  void resetStreak() {
    _currentStreak = 0;
    lastCompletedDate = null;
  }
}

Streak Calculation Logic

The `effectiveStreak` getter in our `Habit` model is crucial. It dynamically calculates the streak based on the `lastCompletedDate` and the current date. If the habit was not completed yesterday or today, the streak effectively resets to zero for display purposes, even if the internal `_currentStreak` value holds a historical high. The `markDone()` method correctly updates the internal `_currentStreak` based on whether the habit was done yesterday or not.

Designing the UI with Flutter

Our UI will consist of a main screen displaying a list of habits, and individual "Habit Tile" widgets for each habit. Each tile will show the habit's name, its current status (done/not done), and the effective streak.

The Habit Tile Widget

This widget will represent a single habit, allowing users to mark it as complete for the day and view its streak.


// lib/widgets/habit_tile.dart
import 'package:flutter/material.dart';
import 'package:habit_tracker/models/habit.dart'; // Ensure correct path

class HabitTile extends StatelessWidget {
  final Habit habit;
  final VoidCallback onToggle;

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

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
      elevation: 2.0,
      child: ListTile(
        contentPadding: const EdgeInsets.all(12.0),
        leading: Checkbox(
          value: habit.isDoneForToday,
          onChanged: (bool? newValue) {
            onToggle();
          },
          activeColor: Colors.deepPurple,
        ),
        title: Text(
          habit.name,
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.w600,
            decoration: habit.isDoneForToday ? TextDecoration.lineThrough : TextDecoration.none,
            color: habit.isDoneForToday ? Colors.grey : Colors.black87,
          ),
        ),
        trailing: Container(
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
          decoration: BoxDecoration(
            color: Colors.deepPurple.shade100,
            borderRadius: BorderRadius.circular(20),
          ),
          child: Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.local_fire_department, color: Colors.orange, size: 18),
              const SizedBox(width: 4),
              Text(
                '${habit.effectiveStreak}',
                style: const TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: Colors.deepPurple,
                ),
              ),
            ],
          ),
        ),
        onTap: onToggle, // Tapping the tile also toggles the habit
      ),
    );
  }
}

The Habit Tracker Screen

This `StatefulWidget` will manage the list of habits and their states. For simplicity, we'll use in-memory state management with `setState`. In a real application, you might use a state management solution like Provider, Riverpod, or BLoC, and persist data to storage.


// lib/screens/habit_tracker_screen.dart
import 'package:flutter/material.dart';
import 'package:habit_tracker/models/habit.dart'; // Ensure correct path
import 'package:habit_tracker/widgets/habit_tile.dart'; // Ensure correct path

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

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

class _HabitTrackerScreenState extends State {
  // Sample habits
  final List _habits = [
    Habit(id: '1', name: 'Drink 2 Liters of Water', creationDate: DateTime.now().subtract(const Duration(days: 5))),
    Habit(id: '2', name: 'Read 20 Pages', creationDate: DateTime.now().subtract(const Duration(days: 3))),
    Habit(id: '3', name: 'Exercise for 30 Mins', creationDate: DateTime.now().subtract(const Duration(days: 10))),
  ];

  void _toggleHabit(Habit habit) {
    setState(() {
      if (habit.isDoneForToday) {
        // If already done today, treat as "undo"
        habit.lastCompletedDate = habit.lastCompletedDate!.subtract(const Duration(days: 1)); // This simplification for undo isn't perfect for streak management,
                                                                                              // a more robust solution would track all completion dates.
                                                                                              // For this article, we focus on the markDone logic.
        // A better undo logic would require tracking the *previous* streak state
        // and last completion date prior to marking today's completion.
        // For simplicity, we'll assume habit is only toggled ON for this demo.
        // The effectiveStreak getter will handle displaying 0 if missed.
      } else {
        habit.markDone();
      }
      // Re-evaluate all habits' effective streaks after a change, especially if using persistence
      _habits.forEach((h) => h.effectiveStreak);
    });
  }

  // A method to simulate daily reset for demonstration if needed,
  // typically handled by the effectiveStreak getter itself.
  void _checkAllHabitsForDailyReset() {
    setState(() {
      // The effectiveStreak getter automatically handles resets when accessed.
      // This explicit loop isn't strictly necessary for the current Habit model,
      // but good to show where daily maintenance logic would go.
      _habits.forEach((habit) {
        if (!habit.isDoneForToday && !habit.lastCompletedDate!.isYesterday(DateTime.now())) {
            // If the habit was missed yesterday and not done today, the effective streak will be 0.
            // The model's effectiveStreak getter handles this without modifying the stored _currentStreak.
        }
      });
    });
  }

  @override
  void initState() {
    super.initState();
    // Simulate some initial completions for demo purposes
    _habits[0].markDone(); // Done today
    _habits[0].markDone(); // Still done today, no change
    
    // Simulate a streak for habit 1
    _habits[0].lastCompletedDate = DateTime.now().subtract(const Duration(days: 1));
    _habits[0].markDone(); // Done today, streak 2
    
    // Simulate an older completion for habit 2
    _habits[1].lastCompletedDate = DateTime.now().subtract(const Duration(days: 3));
    _habits[1].markDone(); // Should restart streak to 1

  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Habits'),
        backgroundColor: Colors.deepPurple,
        foregroundColor: Colors.white,
      ),
      body: ListView.builder(
        itemCount: _habits.length,
        itemBuilder: (context, index) {
          final habit = _habits[index];
          return HabitTile(
            habit: habit,
            onToggle: () => _toggleHabit(habit),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // TODO: Implement "Add New Habit" functionality
          ScaffoldMessenger.of(context).showSnackBar(
            const SnackBar(content: Text('Add New Habit functionality coming soon!')),
          );
        },
        backgroundColor: Colors.deepPurple,
        child: const Icon(Icons.add, color: Colors.white),
      ),
    );
  }
}

Putting It All Together

Finally, we need to set up our `main.dart` file to run the application.


// main.dart
import 'package:flutter/material.dart';
import 'package:habit_tracker/screens/habit_tracker_screen.dart'; // Ensure correct path

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.deepPurple,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: const HabitTrackerScreen(),
    );
  }
}

Enhancements and Further Considerations

This basic structure provides a solid foundation. Here are some ideas for further enhancements:

  • Persistence: Currently, habits are stored in memory and reset on app restart. Implement local storage (e.g., using shared_preferences or Hive) or a backend (e.g., Firebase) to save habit data.
  • Add/Edit Habits: Implement forms to add new habits and modify existing ones.
  • Notifications: Schedule daily reminders to complete habits.
  • Historical View: Display a calendar view to see habit completion history.
  • Advanced Streak Logic: Consider "skip days" or "buffer days" for habits that aren't daily.
  • State Management: For larger apps, replace `setState` with a more robust solution like Provider, Riverpod, or BLoC for cleaner state management across widgets.

Conclusion

Building a habit tracker with a streak counter in Flutter demonstrates the framework's power for creating engaging and functional mobile applications. By carefully designing your data model and implementing logical streak calculation, you can provide users with a compelling tool to stay consistent with their goals. This article has covered the essential components, from data modeling to UI implementation, giving you a strong starting point for your own Flutter habit tracker.

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