image

21 Apr 2026

9K

35K

Building a Habit Tracker Widget with Daily Habit, Streak, and Notification Reminder in Flutter

Habit tracking applications have become indispensable tools for personal development, helping users cultivate positive routines and break detrimental ones. Building a custom habit tracker in Flutter offers immense flexibility and a rich user experience across platforms. This article will guide you through creating a sophisticated habit tracker widget that incorporates daily habit tracking, streak management, and crucial notification reminders, all within the Flutter framework.

1. Project Setup and Dependencies

First, set up a new Flutter project and add the necessary dependencies to your pubspec.yaml file. We'll use provider for state management, shared_preferences for local data persistence, and flutter_local_notifications for scheduling reminders.


dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  provider: ^6.0.5
  shared_preferences: ^2.2.2
  flutter_local_notifications: ^16.3.2
  timezone: ^0.9.2 # Required by flutter_local_notifications

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

After updating pubspec.yaml, run flutter pub get.

2. Defining the Habit Data Model

The core of our application is the Habit model. It needs to store information such as the habit's name, its creation date, a list of completion dates, and a way to calculate the current streak.


import 'dart:convert';

class Habit {
  String id;
  String name;
  DateTime creationDate;
  List<DateTime> completionDates;
  int notificationId;

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

  // Check if the habit was completed today
  bool isCompletedToday() {
    if (completionDates.isEmpty) return false;
    final today = DateTime.now();
    return completionDates.any((date) =>
        date.year == today.year &&
        date.month == today.month &&
        date.day == today.day);
  }

  // Calculate the current streak
  int getCurrentStreak() {
    if (completionDates.isEmpty) return 0;

    final sortedDates = completionDates.map((date) => DateTime(date.year, date.month, date.day)).toList()
      ..sort((a, b) => a.compareTo(b));

    int streak = 0;
    DateTime? lastCompletedDay;

    for (int i = sortedDates.length - 1; i >= 0; i--) {
      final currentDay = sortedDates[i];
      if (lastCompletedDay == null) {
        // If the last completion was today or yesterday
        if (currentDay.isAtSameMomentAs(DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day)) ||
            currentDay.isAtSameMomentAs(DateTime(DateTime.now().year, DateTime.now().month, DateTime.now().day).subtract(Duration(days: 1)))
        ) {
          streak = 1;
          lastCompletedDay = currentDay;
        } else {
          // No recent activity
          return 0;
        }
      } else {
        final dayBeforeLast = lastCompletedDay.subtract(Duration(days: 1));
        if (currentDay.isAtSameMomentAs(dayBeforeLast)) {
          streak++;
          lastCompletedDay = currentDay;
        } else if (currentDay.isBefore(dayBeforeLast)) {
          // Gap in streak
          break;
        }
        // If currentDay is same as lastCompletedDay (duplicate entry), just continue
      }
    }
    return streak;
  }


  // Convert Habit object to a JSON map
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'creationDate': creationDate.toIso8601String(),
      'completionDates': completionDates.map((date) => date.toIso8601String()).toList(),
      'notificationId': notificationId,
    };
  }

  // Create a Habit object from a JSON map
  factory Habit.fromJson(Map<String, dynamic> json) {
    return Habit(
      id: json['id'] as String,
      name: json['name'] as String,
      creationDate: DateTime.parse(json['creationDate'] as String),
      completionDates: (json['completionDates'] as List<dynamic>)
          .map((dateString) => DateTime.parse(dateString as String))
          .toList(),
      notificationId: json['notificationId'] as int,
    );
  }
}

3. State Management and Data Persistence

We'll use Provider for state management and shared_preferences for saving and loading habits. The HabitProvider will manage a list of habits, handle adding new habits, marking completion, and persisting data.


import 'dart:collection';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart'; // Add uuid: ^4.2.1 to pubspec.yaml for unique IDs

import 'habit_model.dart';
import 'notification_service.dart'; // We'll create this next

class HabitProvider extends ChangeNotifier {
  List<Habit> _habits = [];
  final Uuid _uuid = Uuid();

  UnmodifiableListView<Habit> get habits => UnmodifiableListView(_habits);

  HabitProvider() {
    _loadHabits();
  }

  Future<void> _loadHabits() async {
    final prefs = await SharedPreferences.getInstance();
    final habitsJson = prefs.getStringList('habits') ?? [];
    _habits = habitsJson
        .map((jsonString) => Habit.fromJson(json.decode(jsonString)))
        .toList();
    notifyListeners();
  }

  Future<void> _saveHabits() async {
    final prefs = await SharedPreferences.getInstance();
    final habitsJson = _habits.map((habit) => json.encode(habit.toJson())).toList();
    await prefs.setStringList('habits', habitsJson);
  }

  Future<void> addHabit(String name, TimeOfDay reminderTime) async {
    final newNotificationId = DateTime.now().millisecondsSinceEpoch % 100000; // Simple unique ID
    final newHabit = Habit(
      id: _uuid.v4(),
      name: name,
      creationDate: DateTime.now(),
      completionDates: [],
      notificationId: newNotificationId,
    );
    _habits.add(newHabit);
    await NotificationService().scheduleDailyNotification(
      newNotificationId,
      name,
      'Time to ${name.toLowerCase()}!',
      reminderTime,
    );
    await _saveHabits();
    notifyListeners();
  }

  Future<void> toggleHabitCompletion(Habit habit) async {
    final today = DateTime.now();
    final isCompleted = habit.isCompletedToday();

    if (isCompleted) {
      // Remove today's completion
      habit.completionDates.removeWhere((date) =>
          date.year == today.year &&
          date.month == today.month &&
          date.day == today.day);
    } else {
      // Add today's completion
      habit.completionDates.add(today);
    }
    await _saveHabits();
    notifyListeners();
  }

  Future<void> deleteHabit(Habit habit) async {
    _habits.removeWhere((h) => h.id == habit.id);
    await NotificationService().cancelNotification(habit.notificationId);
    await _saveHabits();
    notifyListeners();
  }
}

4. Implementing Notification Reminders

For local notifications, we use flutter_local_notifications. This requires platform-specific setup (e.g., adding permissions in AndroidManifest.xml for Android and configuring capabilities for iOS).

Create a notification_service.dart file:


import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:timezone/data/latest.dart' as tz;

class NotificationService {
  static final NotificationService _notificationService = NotificationService._internal();

  factory NotificationService() {
    return _notificationService;
  }

  NotificationService._internal();

  final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  Future<void> init() async {
    final AndroidInitializationSettings initializationSettingsAndroid =
        AndroidInitializationSettings('@mipmap/ic_launcher');

    final DarwinInitializationSettings initializationSettingsIOS =
        DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
      onDidReceiveLocalNotification: (id, title, body, payload) async {},
    );

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

    tz.initializeTimeZones();

    await flutterLocalNotificationsPlugin.initialize(
      initializationSettings,
      onDidReceiveNotificationResponse: (NotificationResponse response) async {
        // Handle notification tap
      },
      onDidReceiveBackgroundNotificationResponse: (NotificationResponse response) async {
        // Handle notification tap in background
      },
    );
  }

  Future<void> scheduleDailyNotification(
      int id, String title, String body, TimeOfDay time) async {
    await flutterLocalNotificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      _nextInstanceOfTime(time),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'habit_tracker_channel',
          'Habit Reminders',
          channelDescription: 'Daily reminders for your habits',
          importance: Importance.high,
          priority: Priority.high,
          ticker: 'ticker',
        ),
        iOS: DarwinNotificationDetails(),
      ),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
      matchDateTimeComponents: DateTimeComponents.time, // Repeat daily at the specified time
    );
  }

  tz.TZDateTime _nextInstanceOfTime(TimeOfDay time) {
    final tz.TZDateTime now = tz.TZDateTime.now(tz.local);
    tz.TZDateTime scheduledDate = tz.TZDateTime(
      tz.local,
      now.year,
      now.month,
      now.day,
      time.hour,
      time.minute,
    );
    if (scheduledDate.isBefore(now)) {
      scheduledDate = scheduledDate.add(const Duration(days: 1));
    }
    return scheduledDate;
  }

  Future<void> cancelNotification(int id) async {
    await flutterLocalNotificationsPlugin.cancel(id);
  }
}

Initialize the notification service in your main.dart:


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'habit_provider.dart';
import 'home_screen.dart';
import 'notification_service.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await NotificationService().init();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => HabitProvider(),
      child: MaterialApp(
        title: 'Habit Tracker',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const HomeScreen(),
      ),
    );
  }
}

For Android, add this to your AndroidManifest.xml inside the <application> tag:


<!-- Required for FlutterLocalNotificationsPlugin -->
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
        <action android:name="android.intent.action.QUICKBOOT_POWERON"/>
        <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
    </intent-filter>
</receiver>
<!-- For devices running Android 12 (API 31) and higher -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>

5. Building the Habit Widget UI

We'll create two main widgets: HabitTile for individual habits and HomeScreen to display the list and add new habits.

HabitTile Widget

This widget displays a single habit, its streak, and a checkbox to mark completion.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'habit_model.dart';
import 'habit_provider.dart';

class HabitTile extends StatelessWidget {
  final Habit habit;

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

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
      elevation: 2,
      child: ListTile(
        title: Text(
          habit.name,
          style: TextStyle(
            decoration: habit.isCompletedToday() ? TextDecoration.lineThrough : TextDecoration.none,
            fontWeight: FontWeight.bold,
          ),
        ),
        subtitle: Text('Streak: ${habit.getCurrentStreak()} days'),
        trailing: IconButton(
          icon: Icon(
            habit.isCompletedToday() ? Icons.check_box : Icons.check_box_outline_blank,
            color: habit.isCompletedToday() ? Colors.green : Colors.grey,
          ),
          onPressed: () {
            Provider.of<HabitProvider>(context, listen: false)
                .toggleHabitCompletion(habit);
          },
        ),
        onLongPress: () {
          _showDeleteConfirmation(context, habit);
        },
      ),
    );
  }

  void _showDeleteConfirmation(BuildContext context, Habit habit) {
    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('Delete Habit'),
        content: Text('Are you sure you want to delete "${habit.name}"?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(),
            child: const Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              Provider.of<HabitProvider>(context, listen: false).deleteHabit(habit);
              Navigator.of(ctx).pop();
            },
            style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
            child: const Text('Delete'),
          ),
        ],
      ),
    );
  }
}

HomeScreen Widget

This screen displays the list of habits and provides an action button to add new ones.


import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'habit_provider.dart';
import 'habit_tile.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final TextEditingController _habitNameController = TextEditingController();
  TimeOfDay _selectedTime = TimeOfDay.now();

  Future<void> _selectTime(BuildContext context) async {
    final TimeOfDay? picked = await showTimePicker(
      context: context,
      initialTime: _selectedTime,
    );
    if (picked != null && picked != _selectedTime) {
      setState(() {
        _selectedTime = picked;
      });
    }
  }

  void _showAddHabitDialog(BuildContext context) {
    showDialog(
      context: context,
      builder: (ctx) {
        return AlertDialog(
          title: const Text('Add New Habit'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: _habitNameController,
                decoration: const InputDecoration(labelText: 'Habit Name'),
              ),
              const SizedBox(height: 16),
              ListTile(
                title: const Text('Reminder Time'),
                trailing: Text(_selectedTime.format(context)),
                onTap: () => _selectTime(ctx),
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () {
                _habitNameController.clear();
                Navigator.of(ctx).pop();
              },
              child: const Text('Cancel'),
            ),
            ElevatedButton(
              onPressed: () {
                if (_habitNameController.text.isNotEmpty) {
                  Provider.of<HabitProvider>(context, listen: false)
                      .addHabit(_habitNameController.text, _selectedTime);
                  _habitNameController.clear();
                  Navigator.of(ctx).pop();
                }
              },
              child: const Text('Add'),
            ),
          ],
        );
      },
    );
  }

  @override
  void dispose() {
    _habitNameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('My Habits'),
      ),
      body: Consumer<HabitProvider>(
        builder: (context, habitProvider, child) {
          if (habitProvider.habits.isEmpty) {
            return const Center(
              child: Text('No habits yet! Add one to get started.'),
            );
          }
          return ListView.builder(
            itemCount: habitProvider.habits.length,
            itemBuilder: (context, index) {
              final habit = habitProvider.habits[index];
              return HabitTile(habit: habit);
            },
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddHabitDialog(context),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Conclusion

You have now successfully built a foundational habit tracker widget in Flutter, complete with daily habit logging, streak calculation, and persistent notification reminders. This robust structure can be expanded upon in many ways:

  • **Advanced Recurrence**: Implement more complex schedules (weekly, monthly habits).
  • **Statistics & Visualizations**: Add charts and graphs to show progress over time.
  • **Customizable Themes**: Allow users to personalize the app's appearance.
  • **Cloud Sync**: Integrate with a backend service (Firebase, Supabase, etc.) for cross-device synchronization.
  • **More Robust Persistence**: For larger datasets, consider a local database like sqflite or Hive instead of shared_preferences.

By leveraging Flutter's powerful UI capabilities and the rich ecosystem of packages, you can create compelling and useful applications that help users achieve their goals.

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