Building an Event Calendar Widget with Highlighted Dates in Flutter
Event calendars are a fundamental component in many applications, from productivity tools to social platforms. They provide users with a clear visual representation of upcoming events, deadlines, and important dates. In Flutter, building a sophisticated event calendar, complete with date highlighting and event display, can be achieved efficiently using powerful third-party libraries. This article will guide you through the process of creating such a widget, focusing on a professional and maintainable approach.
1. Choosing the Right Library: table_calendar
While one could build a calendar from scratch, leveraging existing, well-maintained libraries is often the most productive path.
For Flutter event calendars, table_calendar stands out as a robust and highly customizable option.
It offers a flexible API for displaying dates, handling selections, and integrating event data.
2. Project Setup
First, add table_calendar to your Flutter project's pubspec.yaml file.
You might also want to add intl for date formatting if not already present, though `table_calendar` handles some formatting internally.
dependencies:
flutter:
sdk: flutter
table_calendar: ^3.0.9 # Use the latest stable version
intl: ^0.18.1 # For date formatting (optional, but good practice)
After adding the dependency, run flutter pub get to fetch the package.
3. Basic Calendar Implementation
A basic TableCalendar can be quickly set up within a StatefulWidget.
This initial setup will display a calendar with no special highlighting or event handling.
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
class EventCalendarScreen extends StatefulWidget {
const EventCalendarScreen({Key? key}) : super(key: key);
@override
_EventCalendarScreenState createState() => _EventCalendarScreenState();
}
class _EventCalendarScreenState extends State {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Event Calendar')),
body: TableCalendar(
firstDay: DateTime.utc(2010, 10, 16),
lastDay: DateTime.utc(2030, 3, 14),
focusedDay: _focusedDay,
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay; // update `_focusedDay` to make sure no day is marked as focused
});
},
calendarFormat: CalendarFormat.month,
headerStyle: const HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
),
calendarStyle: const CalendarStyle(
outsideDaysVisible: false,
),
),
);
}
}
In this snippet:
firstDayandlastDaydefine the scrollable date range.focusedDaydetermines which month is currently displayed.selectedDayPredicateandonDaySelectedare used to manage user selections.
4. Structuring Event Data
To highlight dates with events, we need a way to store our event data.
A simple class for an event and a Map where keys are DateTime objects and values are lists of events for that day is a common and effective pattern.
class Event {
final String title;
const Event(this.title);
@override
String toString() => title;
}
// Example event data structure
// A LinkedHashMap is often used to maintain insertion order,
// but a regular Map> works fine too.
final Map> kEvents = {
DateTime.utc(2023, 11, 15): [const Event('Project Review')],
DateTime.utc(2023, 11, 16): [const Event('Team Meeting'), const Event('Client Call')],
DateTime.utc(2023, 11, 20): [const Event('Conference Day 1')],
DateTime.utc(2023, 11, 21): [const Event('Conference Day 2'), const Event('Networking Event')],
DateTime.utc(2023, 12, 1): [const Event('Holiday Party')],
};
// Helper function to normalize DateTime objects to just year, month, day.
int getHashCode(DateTime key) {
return key.day * 1000000 + key.month * 10000 + key.year;
}
/// Returns a list of [DateTime] objects that are the days in a given week.
List daysInRange(DateTime first, DateTime last) {
final dayCount = last.difference(first).inDays + 1;
return List.generate(
dayCount,
(index) => DateTime.utc(first.year, first.month, first.day + index),
);
}
We use DateTime.utc for consistency, ensuring that date comparisons are independent of local time zones.
5. Highlighting Dates with Events
table_calendar provides an eventLoader callback that takes a DateTime object and returns a List of events for that day.
This list's length is used to display "event dots" under the date.
First, create a helper function to retrieve events for a given day:
List _getEventsForDay(DateTime day) {
// Use a simplified key for the map (year, month, day only)
// Ensure kEvents also uses normalized DateTime objects as keys
return kEvents[DateTime.utc(day.year, day.month, day.day)] ?? [];
}
Now, integrate this into your TableCalendar:
// Inside your TableCalendar widget's build method
TableCalendar(
// ... other properties ...
eventLoader: _getEventsForDay, // Add this line
calendarBuilders: CalendarBuilders(
markerBuilder: (context, day, events) {
if (events.isNotEmpty) {
return Positioned(
right: 1,
bottom: 1,
child: _buildEventsMarker(events),
);
}
return null;
},
),
// ...
);
// Helper method for the markerBuilder
Widget _buildEventsMarker(List events) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue[400],
),
width: 16.0,
height: 16.0,
child: Center(
child: Text(
'${events.length}',
style: const TextStyle().copyWith(
color: Colors.white,
fontSize: 12.0,
),
),
),
);
}
The markerBuilder gives you full control over how event markers are displayed. Here, we're showing a simple blue circle with the count of events.
6. Displaying Events for the Selected Date
Typically, when a user selects a date, they expect to see the details of events scheduled for that day. We can display these events in a list below the calendar.
// Add this list to your _EventCalendarScreenState
List _selectedEvents = [];
// Modify _onDaySelected to update _selectedEvents
@override
void initState() {
super.initState();
// Initialize _selectedDay and _selectedEvents
_selectedDay = _focusedDay;
_selectedEvents = _getEventsForDay(_selectedDay!);
}
// Update the onDaySelected callback
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
_selectedEvents = _getEventsForDay(selectedDay); // Update events for the selected day
});
},
// Below the TableCalendar, add an Expanded ListView to show events
// ...
body: Column(
children: [
TableCalendar(
// ... calendar properties ...
),
const SizedBox(height: 8.0),
Expanded(
child: ListView.builder(
itemCount: _selectedEvents.length,
itemBuilder: (context, index) {
final event = _selectedEvents[index];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
decoration: BoxDecoration(
border: Border.all(),
borderRadius: BorderRadius.circular(12.0),
),
child: ListTile(
onTap: () => print('Event tapped: ${event.title}'),
title: Text(event.title),
),
);
},
),
),
],
),
7. Putting It All Together (Full Example)
Here's the complete code for the EventCalendarScreen demonstrating all the concepts discussed.
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart'; // For date formatting if needed, though not strictly used in this example.
class EventCalendarScreen extends StatefulWidget {
const EventCalendarScreen({Key? key}) : super(key: key);
@override
_EventCalendarScreenState createState() => _EventCalendarScreenState();
}
class Event {
final String title;
const Event(this.title);
@override
String toString() => title;
}
// Example event data structure
// Ensure all DateTime keys are normalized to UTC with hour/minute/second set to 0.
final Map> kEvents = {
DateTime.utc(2023, 11, 15): [const Event('Project Review')],
DateTime.utc(2023, 11, 16): [const Event('Team Meeting'), const Event('Client Call')],
DateTime.utc(2023, 11, 20): [const Event('Conference Day 1')],
DateTime.utc(2023, 11, 21): [const Event('Conference Day 2'), const Event('Networking Event')],
DateTime.utc(2023, 12, 1): [const Event('Holiday Party')],
DateTime.utc(2023, 12, 10): [const Event('Annual Checkup')],
DateTime.utc(2024, 1, 5): [const Event('New Year Planning')],
};
class _EventCalendarScreenState extends State {
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
List _selectedEvents = [];
@override
void initState() {
super.initState();
_selectedDay = _focusedDay;
_selectedEvents = _getEventsForDay(_selectedDay!);
}
List _getEventsForDay(DateTime day) {
// Normalize the day to match the keys in kEvents (year, month, day only).
return kEvents[DateTime.utc(day.year, day.month, day.day)] ?? [];
}
void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay; // update `_focusedDay` to make sure no day is marked as focused
_selectedEvents = _getEventsForDay(selectedDay);
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Event Calendar')),
body: Column(
children: [
TableCalendar(
firstDay: DateTime.utc(2010, 10, 16),
lastDay: DateTime.utc(2030, 3, 14),
focusedDay: _focusedDay,
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onDaySelected: _onDaySelected,
eventLoader: _getEventsForDay,
calendarFormat: CalendarFormat.month,
headerStyle: const HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
),
calendarStyle: CalendarStyle(
outsideDaysVisible: false,
todayDecoration: BoxDecoration(
color: Colors.blue.withOpacity(0.2),
shape: BoxShape.circle,
),
selectedDecoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
markerDecoration: BoxDecoration(
color: Colors.red[400],
shape: BoxShape.circle,
),
),
calendarBuilders: CalendarBuilders(
markerBuilder: (context, day, events) {
if (events.isNotEmpty) {
return Positioned(
right: 1,
bottom: 1,
child: _buildEventsMarker(events),
);
}
return null;
},
),
),
const SizedBox(height: 8.0),
Expanded(
child: _selectedEvents.isEmpty
? const Center(child: Text('No events for this day.'))
: ListView.builder(
itemCount: _selectedEvents.length,
itemBuilder: (context, index) {
final event = _selectedEvents[index];
return Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12.0),
),
child: ListTile(
onTap: () {
// Handle event tap, e.g., navigate to event details screen
print('Event tapped: ${event.title}');
},
title: Text(event.title),
leading: const Icon(Icons.event),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
),
);
},
),
),
],
),
);
}
Widget _buildEventsMarker(List events) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red[400], // Marker color for days with events
),
width: 16.0,
height: 16.0,
child: Center(
child: Text(
'${events.length}',
style: const TextStyle().copyWith(
color: Colors.white,
fontSize: 10.0,
),
),
),
);
}
}
Conclusion
Building an interactive event calendar with highlighted dates in Flutter is significantly streamlined by using the table_calendar package.
By properly structuring your event data, implementing the eventLoader, and handling date selections, you can create a feature-rich calendar that enhances user experience.
The customizability of table_calendar allows you to tailor its appearance and behavior to perfectly match your application's design language and functional requirements.