image

18 Apr 2026

9K

35K

Building a Goal Tracker Widget with Weekly, Monthly, and Streak View in Flutter

Introduction

Tracking personal goals is a powerful habit for self-improvement and productivity. In application development, a well-designed goal tracker widget can significantly enhance user engagement and provide clear insights into progress. This article details the professional approach to building a comprehensive goal tracker widget in Flutter, incorporating weekly, monthly, and streak views to offer users diverse perspectives on their achievements.

Our objective is to create a modular, maintainable, and visually informative widget that allows users to mark daily progress, review their history over different timeframes, and celebrate consecutive accomplishments through a streak counter.

Core Principles and Architecture

To build a robust goal tracker, we'll adhere to several core principles:

  • Data-Driven Design: The UI will reflect the underlying data model, ensuring consistency and ease of updates.
  • Modularity: Breaking down the widget into smaller, focused components (e.g., `WeeklyView`, `MonthlyView`, `StreakView`) improves maintainability and reusability.
  • State Management: A clear strategy for managing the state of goals and their completion status is crucial for a responsive user experience. For simplicity in this article, we'll use `StatefulWidget` with `setState`, but more advanced solutions like Provider, BLoC, or Riverpod could be integrated.
  • User Experience (UX): Visual cues for completion, clear navigation, and an intuitive interface are paramount.

Defining the Data Model

The foundation of our goal tracker is a well-defined data model. We need classes to represent the goal itself and individual daily entries indicating completion.


import 'package:flutter/material.dart';

class Goal {
  String id;
  String title;
  String description;
  List<GoalEntry> entries; // Daily completion entries

  Goal({
    required this.id,
    required this.title,
    this.description = '',
    List<GoalEntry>? entries,
  }) : entries = entries ?? [];

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

  // Toggle completion status for a given date
  void toggleCompletion(DateTime date) {
    final existingEntryIndex = entries.indexWhere((entry) =>
        entry.date.year == date.year &&
        entry.date.month == date.month &&
        entry.date.day == date.day);

    if (existingEntryIndex != -1) {
      entries.removeAt(existingEntryIndex); // Remove if already completed
    } else {
      entries.add(GoalEntry(date: date, completed: true)); // Add if not completed
    }
  }

  // For simplicity, we'll sort entries by date
  void sortEntries() {
    entries.sort((a, b) => a.date.compareTo(b.date));
  }
}

class GoalEntry {
  DateTime date;
  bool completed;

  GoalEntry({required this.date, this.completed = false});
}

Structuring the Goal Tracker Widget

The main `GoalTrackerWidget` will be a `StatefulWidget` to manage the goal data and trigger UI updates. It will hold the `Goal` object and delegate rendering to its sub-views.


import 'package:flutter/material.dart';
// Import Goal and GoalEntry from your data model file

class GoalTrackerWidget extends StatefulWidget {
  final Goal goal;

  const GoalTrackerWidget({Key? key, required this.goal}) : super(key: key);

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

class _GoalTrackerWidgetState extends State<GoalTrackerWidget> {
  late Goal _currentGoal;
  DateTime _selectedDate = DateTime.now(); // For view context

  @override
  void initState() {
    super.initState();
    _currentGoal = widget.goal;
    _currentGoal.sortEntries(); // Ensure entries are sorted
  }

  void _toggleGoalCompletion(DateTime date) {
    setState(() {
      _currentGoal.toggleCompletion(date);
      _currentGoal.sortEntries(); // Re-sort after modification
    });
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16.0),
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              _currentGoal.title,
              style: Theme.of(context).textTheme.headline6,
            ),
            const SizedBox(height: 8),
            Text(_currentGoal.description),
            const SizedBox(height: 16),
            // Streak View
            StreakView(goal: _currentGoal),
            const SizedBox(height: 16),
            // Weekly View
            WeeklyView(
              goal: _currentGoal,
              selectedDate: _selectedDate,
              onDateTapped: _toggleGoalCompletion,
            ),
            const SizedBox(height: 16),
            // Monthly View
            MonthlyView(
              goal: _currentGoal,
              selectedDate: _selectedDate,
              onDateTapped: _toggleGoalCompletion,
            ),
          ],
        ),
      ),
    );
  }
}

Implementing the Weekly View

The `WeeklyView` will display the days of the current week, allowing users to quickly see and update their progress for each day. Tapping a day will toggle its completion status.


import 'package:flutter/material.dart';
// Import Goal from your data model file

class WeeklyView extends StatelessWidget {
  final Goal goal;
  final DateTime selectedDate;
  final Function(DateTime) onDateTapped;

  const WeeklyView({
    Key? key,
    required this.goal,
    required this.selectedDate,
    required this.onDateTapped,
  }) : super(key: key);

  List<DateTime> _getCurrentWeekDates(DateTime date) {
    List<DateTime> weekDates = [];
    DateTime firstDayOfWeek = date.subtract(Duration(days: date.weekday - 1)); // Monday
    for (int i = 0; i < 7; i++) {
      weekDates.add(firstDayOfWeek.add(Duration(days: i)));
    }
    return weekDates;
  }

  @override
  Widget build(BuildContext context) {
    final List<DateTime> weekDates = _getCurrentWeekDates(selectedDate);

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Weekly Progress', style: Theme.of(context).textTheme.subtitle1),
        const SizedBox(height: 8),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceAround,
          children: weekDates.map((date) {
            final bool isCompleted = goal.isCompletedOn(date);
            final bool isToday = date.year == DateTime.now().year &&
                date.month == DateTime.now().month &&
                date.day == DateTime.now().day;

            return GestureDetector(
              onTap: () => onDateTapped(date),
              child: Container(
                padding: const EdgeInsets.all(8),
                decoration: BoxDecoration(
                  color: isCompleted
                      ? Colors.green.shade300
                      : (isToday ? Colors.blue.shade100 : Colors.grey.shade200),
                  borderRadius: BorderRadius.circular(8),
                  border: isToday ? Border.all(color: Colors.blueAccent, width: 2) : null,
                ),
                child: Column(
                  children: [
                    Text(
                      _getWeekdayShortName(date.weekday),
                      style: TextStyle(
                        color: isCompleted ? Colors.white : Colors.black87,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                    Text(
                      date.day.toString(),
                      style: TextStyle(
                        color: isCompleted ? Colors.white : Colors.black87,
                      ),
                    ),
                  ],
                ),
              ),
            );
          }).toList(),
        ),
      ],
    );
  }

  String _getWeekdayShortName(int weekday) {
    switch (weekday) {
      case 1: return 'Mon';
      case 2: return 'Tue';
      case 3: return 'Wed';
      case 4: return 'Thu';
      case 5: return 'Fri';
      case 6: return 'Sat';
      case 7: return 'Sun';
      default: return '';
    }
  }
}

Implementing the Monthly View

The `MonthlyView` provides a calendar-like grid for the selected month, allowing users to see their progress over a longer period. It highlights completed days and provides interaction for toggling completion.


import 'package:flutter/material.dart';
// Import Goal from your data model file

class MonthlyView extends StatelessWidget {
  final Goal goal;
  final DateTime selectedDate;
  final Function(DateTime) onDateTapped;

  const MonthlyView({
    Key? key,
    required this.goal,
    required this.selectedDate,
    required this.onDateTapped,
  }) : super(key: key);

  List<DateTime> _getDaysInMonth(DateTime date) {
    final List<DateTime> days = [];
    final DateTime firstDayOfMonth = DateTime(date.year, date.month, 1);
    final DateTime lastDayOfMonth = DateTime(date.year, date.month + 1, 0);

    // Add leading empty days for alignment (e.g., if month starts on Wednesday)
    int firstWeekday = firstDayOfMonth.weekday; // Monday is 1, Sunday is 7
    for (int i = 1; i < firstWeekday; i++) {
      days.add(DateTime(0,0,0)); // Placeholder for empty cells
    }

    for (int i = 1; i <= lastDayOfMonth.day; i++) {
      days.add(DateTime(date.year, date.month, i));
    }
    return days;
  }

  @override
  Widget build(BuildContext context) {
    final List<DateTime> daysInMonth = _getDaysInMonth(selectedDate);
    final List<String> weekdayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];

    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Monthly Progress (${selectedDate.year}/${selectedDate.month})',
            style: Theme.of(context).textTheme.subtitle1),
        const SizedBox(height: 8),
        Table(
          children: [
            TableRow(
              children: weekdayNames.map((name) => Center(child: Text(name, style: TextStyle(fontWeight: FontWeight.bold)))).toList(),
            ),
            ...List.generate((daysInMonth.length / 7).ceil(), (weekIndex) {
              return TableRow(
                children: List.generate(7, (dayIndex) {
                  final int dateIndex = weekIndex * 7 + dayIndex;
                  if (dateIndex < daysInMonth.length && daysInMonth[dateIndex].year != 0) { // Check for valid date (not placeholder)
                    final DateTime day = daysInMonth[dateIndex];
                    final bool isCompleted = goal.isCompletedOn(day);
                    final bool isToday = day.year == DateTime.now().year &&
                        day.month == DateTime.now().month &&
                        day.day == DateTime.now().day;

                    return GestureDetector(
                      onTap: () => onDateTapped(day),
                      child: Container(
                        margin: const EdgeInsets.all(2),
                        alignment: Alignment.center,
                        height: 36,
                        decoration: BoxDecoration(
                          color: isCompleted
                              ? Colors.green.shade300
                              : (isToday ? Colors.blue.shade100 : Colors.grey.shade100),
                          borderRadius: BorderRadius.circular(4),
                          border: isToday ? Border.all(color: Colors.blueAccent, width: 1.5) : null,
                        ),
                        child: Text(
                          day.day.toString(),
                          style: TextStyle(
                            color: isCompleted ? Colors.white : Colors.black87,
                          ),
                        ),
                      ),
                    );
                  } else {
                    return Container(); // Empty cell for alignment
                  }
                }),
              );
            }),
          ],
        ),
      ],
    );
  }
}

Implementing the Streak View

The `StreakView` calculates and displays the current consecutive streak of goal completions. This motivates users by highlighting their consistent effort.


import 'package:flutter/material.dart';
// Import Goal from your data model file

class StreakView extends StatelessWidget {
  final Goal goal;

  const StreakView({Key? key, required this.goal}) : super(key: key);

  int _calculateCurrentStreak(Goal goal) {
    if (goal.entries.isEmpty) {
      return 0;
    }

    int currentStreak = 0;
    DateTime today = DateTime.now();
    DateTime yesterday = today.subtract(const Duration(days: 1));

    // Check if goal was completed today or yesterday
    bool completedToday = goal.isCompletedOn(today);
    bool completedYesterday = goal.isCompletedOn(yesterday);

    if (!completedToday && !completedYesterday) {
      return 0; // Streak broken or not started recently
    }

    // Start checking from today backwards
    DateTime checkDate = today;
    if (!completedToday) {
      // If not completed today, but was completed yesterday, streak ends yesterday
      checkDate = yesterday;
    }

    // Iterate backwards as long as goal was completed
    while (goal.isCompletedOn(checkDate)) {
      currentStreak++;
      checkDate = checkDate.subtract(const Duration(days: 1));
    }

    return currentStreak;
  }

  @override
  Widget build(BuildContext context) {
    final int streak = _calculateCurrentStreak(goal);

    return Row(
      children: [
        Icon(Icons.local_fire_department, color: Colors.orange, size: 28),
        const SizedBox(width: 8),
        Text(
          'Current Streak: $streak days!',
          style: Theme.of(context).textTheme.headline6?.copyWith(color: Colors.orange),
        ),
      ],
    );
  }
}

Integrating Views and Displaying Goals

The `_GoalTrackerWidgetState`'s `build` method orchestrates the display of these views. Each view receives the `Goal` object and the `_toggleGoalCompletion` callback, ensuring that user interactions are propagated back to the state management layer.

To use this widget, you would instantiate it with a `Goal` object, potentially loaded from a database or initial dummy data:


import 'package:flutter/material.dart';
// Import Goal and GoalTrackerWidget from your files

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Example Goal data
    final Goal myDailyGoal = Goal(
      id: 'goal_001',
      title: 'Drink 8 Glasses of Water',
      description: 'Stay hydrated throughout the day.',
      entries: [
        GoalEntry(date: DateTime.now().subtract(const Duration(days: 3)), completed: true),
        GoalEntry(date: DateTime.now().subtract(const Duration(days: 2)), completed: true),
        GoalEntry(date: DateTime.now().subtract(const Duration(days: 1)), completed: true),
        GoalEntry(date: DateTime.now().subtract(const Duration(days: 7)), completed: true),
        // Add more entries as needed
      ],
    );

    return MaterialApp(
      title: 'Goal Tracker Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('My Goals'),
        ),
        body: SingleChildScrollView(
          child: Column(
            children: [
              GoalTrackerWidget(goal: myDailyGoal),
              // You can add more GoalTrackerWidgets for other goals here
              // GoalTrackerWidget(goal: anotherGoal),
            ],
          ),
        ),
      ),
    );
  }
}

Conclusion

Building a goal tracker widget in Flutter with weekly, monthly, and streak views involves careful data modeling, modular UI development, and effective state management. By breaking down the problem into smaller, manageable components—the `Goal` data model, `GoalTrackerWidget` as the orchestrator, and dedicated `WeeklyView`, `MonthlyView`, and `StreakView` widgets—we achieve a flexible and scalable solution.

This architecture provides a clear path for further enhancements, such as persistent storage (using SQLite, Firebase, or shared preferences), goal editing, notifications, or more advanced analytics. The professional approach outlined here ensures a robust foundation for an engaging and motivating goal-tracking experience.

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