image

01 May 2026

9K

35K

Crafting a Flutter Habit Tracker Widget with Daily Habits, Streaks, Notifications, and Reminders

Habit trackers are powerful tools that help individuals build and maintain positive routines. By visualizing progress, they provide motivation and accountability. In this article, we'll explore how to develop a sophisticated habit tracker widget in Flutter, incorporating essential features like daily habit tracking, streak calculation, local notifications, and intelligent reminders.

1. Understanding the Core Components

To build our habit tracker, we'll need a robust data model and a clear understanding of state management. We'll represent each habit as an object and manage a list of these habits within our widget.

1.1 The Habit Data Model

Our Habit class will encapsulate all the necessary information for a single habit, including its name, current completion status, streak, and the last time it was successfully completed.


import 'package:flutter/material.dart';

class Habit {
  String id;
  String name;
  bool isCompletedToday;
  int streak;
  DateTime? lastCompletedDate;
  TimeOfDay? reminderTime; // For scheduling notifications

  Habit({
    required this.id,
    required this.name,
    this.isCompletedToday = false,
    this.streak = 0,
    this.lastCompletedDate,
    this.reminderTime,
  });

  // Factory constructor for creating a Habit from a Map (e.g., for persistence)
  factory Habit.fromMap(Map map) {
    return Habit(
      id: map['id'],
      name: map['name'],
      isCompletedToday: map['isCompletedToday'],
      streak: map['streak'],
      lastCompletedDate: map['lastCompletedDate'] != null
          ? DateTime.parse(map['lastCompletedDate'])
          : null,
      reminderTime: map['reminderTime'] != null
          ? TimeOfDay(
              hour: (map['reminderTime'] as Map)['hour'],
              minute: (map['reminderTime'] as Map)['minute'],
            )
          : null,
    );
  }

  // Convert Habit to a Map (e.g., for persistence)
  Map toMap() {
    return {
      'id': id,
      'name': name,
      'isCompletedToday': isCompletedToday,
      'streak': streak,
      'lastCompletedDate': lastCompletedDate?.toIso8601String(),
      'reminderTime': reminderTime != null
          ? {'hour': reminderTime!.hour, 'minute': reminderTime!.minute}
          : null,
    };
  }
}
  

1.2 Habit Tracker Widget Structure

We'll use a StatefulWidget to manage the list of habits and their completion status. For a production app, state management solutions like Provider, Riverpod, or BLoC might be preferred, but for this example, local state is sufficient to demonstrate the core logic.


import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:uuid/uuid.dart'; // Add uuid package to pubspec.yaml

// Placeholder for our Habit class (defined above)
// class Habit { ... }

class HabitTrackerWidget extends StatefulWidget {
  const HabitTrackerWidget({super.key});

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

class _HabitTrackerWidgetState extends State {
  final List<Habit> _habits = [];
  final Uuid _uuid = const Uuid();
  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  @override
  void initState() {
    super.initState();
    _initializeNotifications();
    _loadSampleHabits(); // Load some initial habits
    _resetDailyCompletionStatus(); // Ensure habits are reset at start
  }

  // ... (Notification initialization and other methods will go here)

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            'Your Habits',
            style: Theme.of(context).textTheme.headlineMedium,
          ),
        ),
        Expanded(
          child: _habits.isEmpty
              ? const Center(child: Text('No habits added yet!'))
              : ListView.builder(
                  itemCount: _habits.length,
                  itemBuilder: (context, index) {
                    final habit = _habits[index];
                    return Card(
                      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                      child: ListTile(
                        leading: Checkbox(
                          value: habit.isCompletedToday,
                          onChanged: (bool? newValue) {
                            if (newValue != null) {
                              _toggleHabitCompletion(habit, newValue);
                            }
                          },
                        ),
                        title: Text(
                          habit.name,
                          style: TextStyle(
                            decoration: habit.isCompletedToday
                                ? TextDecoration.lineThrough
                                : TextDecoration.none,
                          ),
                        ),
                        subtitle: Text('Streak: ${habit.streak} days'),
                        trailing: IconButton(
                          icon: const Icon(Icons.alarm),
                          onPressed: () => _showReminderPicker(habit),
                        ),
                        onLongPress: () => _deleteHabit(habit),
                      ),
                    );
                  },
                ),
        ),
        FloatingActionButton(
          onPressed: _addHabit,
          child: const Icon(Icons.add),
        ),
      ],
    );
  }
}
  

2. Daily Habit Tracking and Streak Calculation

The core functionality involves marking a habit as complete for the day and updating its streak. The streak logic needs to consider if the habit was completed yesterday, today, or if a day was missed.


// Inside _HabitTrackerWidgetState
void _toggleHabitCompletion(Habit habit, bool isCompleted) {
  setState(() {
    final now = DateTime.now();
    final today = DateTime(now.year, now.month, now.day);

    habit.isCompletedToday = isCompleted;

    if (isCompleted) {
      if (habit.lastCompletedDate == null) {
        // First completion
        habit.streak = 1;
      } else {
        final lastCompletionDay = DateTime(
            habit.lastCompletedDate!.year,
            habit.lastCompletedDate!.month,
            habit.lastCompletedDate!.day);
        final yesterday = today.subtract(const Duration(days: 1));

        if (lastCompletionDay.isAtSameMomentAs(yesterday)) {
          // Completed yesterday, continue streak
          habit.streak++;
        } else if (!lastCompletionDay.isAtSameMomentAs(today)) {
          // Missed a day, but completed today
          habit.streak = 1;
        }
        // If lastCompletionDay is today, do nothing (already counted)
      }
      habit.lastCompletedDate = now;
    } else {
      // Habit marked as incomplete
      // For simplicity, we'll only reset streak if it was completed today
      // More complex logic might involve checking if it was completed yesterday etc.
      if (habit.lastCompletedDate != null) {
        final lastCompletionDay = DateTime(
            habit.lastCompletedDate!.year,
            habit.lastCompletedDate!.month,
            habit.lastCompletedDate!.day);
        if (lastCompletionDay.isAtSameMomentAs(today)) {
          // If it was completed today, and now marked incomplete, reset streak
          // or revert streak based on previous day. For simplicity, reset to 0
          habit.streak = 0;
          habit.lastCompletedDate = null; // Or to previous valid completion
        }
      }
    }
  });
  // You would typically persist this data here (e.g., shared_preferences)
  _scheduleOrCancelNotification(habit); // Update notification status
}

// This method should be called once per day (e.g., on app launch after a day change)
void _resetDailyCompletionStatus() {
  final now = DateTime.now();
  final today = DateTime(now.year, now.month, now.day);
  setState(() {
    for (var habit in _habits) {
      if (habit.lastCompletedDate == null ||
          !DateTime(habit.lastCompletedDate!.year,
                  habit.lastCompletedDate!.month,
                  habit.lastCompletedDate!.day)
              .isAtSameMomentAs(today)) {
        habit.isCompletedToday = false;
        // Optionally, reset streak if a day was definitely missed.
        // This is a simplified approach. A more robust solution might
        // check if lastCompletedDate was yesterday.
        if (habit.lastCompletedDate != null &&
            !DateTime(habit.lastCompletedDate!.year,
                    habit.lastCompletedDate!.month,
                    habit.lastCompletedDate!.day)
                .isAtSameMomentAs(today.subtract(const Duration(days: 1)))) {
          habit.streak = 0;
        }
      }
    }
  });
}

// Add/Delete habit methods
void _addHabit() async {
  TextEditingController habitNameController = TextEditingController();
  await showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: const Text('Add New Habit'),
        content: TextField(
          controller: habitNameController,
          decoration: const InputDecoration(hintText: 'Enter habit name'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Cancel'),
          ),
          TextButton(
            onPressed: () {
              if (habitNameController.text.isNotEmpty) {
                setState(() {
                  _habits.add(Habit(
                    id: _uuid.v4(),
                    name: habitNameController.text,
                  ));
                });
                Navigator.pop(context);
              }
            },
            child: const Text('Add'),
          ),
        ],
      );
    },
  );
  // Persist habits after adding
}

void _deleteHabit(Habit habit) {
  setState(() {
    _habits.removeWhere((h) => h.id == habit.id);
  });
  _cancelNotification(habit); // Cancel any pending notifications
  // Persist habits after deleting
}
  

3. Local Notifications and Reminders

To keep users engaged, we'll implement local notifications using the flutter_local_notifications package. This allows us to schedule reminders for specific habits at chosen times.

First, add the package to your pubspec.yaml:


dependencies:
  flutter:
    sdk: flutter
  flutter_local_notifications: ^17.1.2
  timezone: ^0.9.3
  uuid: ^4.4.0 # For generating unique IDs for habits
  # Add other dependencies like shared_preferences for persistence if needed
  

Then, set up the necessary platform configurations for notifications (e.g., adding permissions to AndroidManifest.xml for Android or configuring capabilities for iOS).


// Inside _HabitTrackerWidgetState

Future<void> _initializeNotifications() async {
  // Android initialization
  const AndroidInitializationSettings initializationSettingsAndroid =
      AndroidInitializationSettings('@mipmap/ic_launcher');

  // iOS initialization
  const DarwinInitializationSettings initializationSettingsIOS =
      DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );

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

  await flutterLocalNotificationsPlugin.initialize(
    initializationSettings,
    onDidReceiveNotificationResponse:
        (NotificationResponse notificationResponse) async {
      // Handle tap on notification here
      // E.g., navigate to habit details or mark complete
    },
    onDidReceiveBackgroundNotificationResponse:
        (NotificationResponse notificationResponse) async {
      // Handle tap on background notification
    },
  );

  // Request permissions for Android 13+
  await flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<
          AndroidFlutterLocalNotificationsPlugin>()
      ?.requestNotificationsPermission();

  // Set timezone for scheduled notifications (important for accurate scheduling)
  // Ensure you've imported 'package:timezone/data/latest.dart' and 'package:timezone/timezone.dart' as tz;
  // tz.initializeTimeZones();
  // tz.setLocalLocation(tz.getLocation('America/New_York')); // Or appropriate timezone
}

// Show time picker and schedule notification
Future<void> _showReminderPicker(Habit habit) async {
  final TimeOfDay? pickedTime = await showTimePicker(
    context: context,
    initialTime: habit.reminderTime ?? TimeOfDay.now(),
  );

  if (pickedTime != null) {
    setState(() {
      habit.reminderTime = pickedTime;
    });
    _scheduleOrCancelNotification(habit);
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text('Reminder set for ${habit.name} at ${pickedTime.format(context)}')),
    );
  }
}

// Schedule or cancel notification based on habit's reminder time
Future<void> _scheduleOrCancelNotification(Habit habit) async {
  _cancelNotification(habit); // Always cancel existing first to avoid duplicates

  if (habit.reminderTime != null && !habit.isCompletedToday) {
    // Schedule the notification only if reminder time is set AND not completed today
    final now = DateTime.now();
    DateTime scheduledDate = DateTime(
      now.year,
      now.month,
      now.day,
      habit.reminderTime!.hour,
      habit.reminderTime!.minute,
    );

    // If the scheduled time is in the past for today, schedule for tomorrow
    if (scheduledDate.isBefore(now)) {
      scheduledDate = scheduledDate.add(const Duration(days: 1));
    }

    const AndroidNotificationDetails androidPlatformChannelSpecifics =
        AndroidNotificationDetails(
      'habit_tracker_channel', // Channel ID
      'Habit Reminders',      // Channel Name
      channelDescription: 'Reminders for your daily habits',
      importance: Importance.high,
      priority: Priority.high,
      ticker: 'ticker',
    );
    const NotificationDetails platformChannelSpecifics = NotificationDetails(
      android: androidPlatformChannelSpecifics,
      iOS: DarwinNotificationDetails(),
    );

    await flutterLocalNotificationsPlugin.zonedSchedule(
      habit.id.hashCode, // A unique ID for the notification
      'Time to ${habit.name}!',
      "Don't forget to complete your habit today.",
      tz.TZDateTime.from(scheduledDate, tz.local),
      platformChannelSpecifics,
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation:
          UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at this time
    );
  }
}

Future<void> _cancelNotification(Habit habit) async {
  await flutterLocalNotificationsPlugin.cancel(habit.id.hashCode);
}

// Sample Habits for demonstration
void _loadSampleHabits() {
  setState(() {
    _habits.add(Habit(id: _uuid.v4(), name: 'Drink 8 glasses of water', reminderTime: TimeOfDay(hour: 10, minute: 0)));
    _habits.add(Habit(id: _uuid.v4(), name: 'Read 30 minutes', reminderTime: TimeOfDay(hour: 20, minute: 0)));
    _habits.add(Habit(id: _uuid.v4(), name: 'Exercise for 1 hour'));
  });
  // Schedule notifications for initial habits
  for (var habit in _habits) {
    _scheduleOrCancelNotification(habit);
  }
}
  

4. Persistence (Beyond this article's scope, but crucial)

For a real-world application, the habits and their states must be persisted. Otherwise, all data will be lost when the app closes. Common solutions include:

  • shared_preferences: Simple key-value storage for small data.
  • sqflite: A SQLite database for structured data.
  • Firebase Firestore/Realtime Database: Cloud-based NoSQL database for syncing data across devices.

You would integrate saving and loading logic within initState(), after adding/updating/deleting habits, and potentially before the app is paused/closed.

Conclusion

We've walked through the fundamental steps of building a functional habit tracker widget in Flutter. By defining a clear data model, implementing robust logic for daily completion and streak calculation, and integrating local notifications for reminders, we can create a powerful tool to foster positive habits. Further enhancements could include customizable habit colors, historical data views, weekly targets, and more advanced statistics.

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