Building an Interactive Calendar Widget in Flutter
Interactive calendar widgets are an indispensable component in modern applications, providing users with an intuitive way to view dates, schedule events, and manage their time. From booking systems and task managers to personal organizers, a well-implemented calendar significantly enhances user experience. Flutter, with its rich set of widgets and declarative UI approach, makes creating such interactive elements a streamlined process. This article will guide you through building a dynamic and customizable calendar widget using Flutter, leveraging the popular table_calendar package.
Prerequisites
Before diving in, ensure you have a basic understanding of Flutter development, including:
- Flutter SDK installed and configured.
- Familiarity with Dart programming language.
- Knowledge of StatelessWidget and StatefulWidget concepts.
Integrating table_calendar
The table_calendar package is a feature-rich, highly customizable Flutter calendar widget. To begin, add it to your pubspec.yaml file under the dependencies section:
dependencies:
flutter:
sdk: flutter
table_calendar: ^3.0.9 # Use the latest version
After adding the dependency, run flutter pub get in your terminal to fetch the package.
Building the Basic Calendar Widget
Let's start by creating a simple stateful widget that displays a basic calendar. We'll use StatefulWidget because our calendar will need to manage its internal state, such as the currently selected or focused day.
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
class CalendarPage extends StatefulWidget {
const CalendarPage({super.key});
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Interactive Calendar'),
),
body: TableCalendar(
firstDay: DateTime.utc(2010, 10, 16),
lastDay: DateTime.utc(2030, 3, 14),
focusedDay: _focusedDay,
calendarFormat: _calendarFormat,
selectedDayPredicate: (day) {
// Use `selectedDayPredicate` to determine which days should be marked as selected
return isSameDay(_selectedDay, day);
},
onDaySelected: (selectedDay, focusedDay) {
if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay; // update `_focusedDay` as well
});
}
},
onFormatChanged: (format) {
if (_calendarFormat != format) {
setState(() {
_calendarFormat = format;
});
}
},
onPageChanged: (focusedDay) {
// No need to call setState, it will be called inside `onDaySelected`
_focusedDay = focusedDay;
},
),
);
}
}
In this basic setup:
firstDayandlastDaydefine the valid date range for the calendar.focusedDaydetermines which month/year is currently displayed.calendarFormatcontrols whether the calendar shows month, two weeks, or week view.selectedDayPredicateis a callback that highlights a day if it matches the_selectedDaystate.onDaySelectedis triggered when a user taps a day, updating the_selectedDayand_focusedDaystate variables.
Enhancing Interactivity: Date Selection and Focus
The core of an interactive calendar lies in its ability to respond to user input. The onDaySelected callback is crucial for this. When a day is selected, we update the _selectedDay variable and then trigger a UI rebuild using setState(). The selectedDayPredicate then uses this updated state to visually highlight the chosen day.
The _focusedDay variable ensures that the calendar view stays consistent, either on the initially focused month or updates when the user navigates between months or selects a day in a different month.
Customizing the Calendar's Appearance
table_calendar offers extensive customization options to match your application's theme. You can customize the header, individual day cells, event markers, and more.
// Inside your TableCalendar widget's build method:
TableCalendar(
// ... (previous properties)
headerStyle: const HeaderStyle(
formatButtonVisible: false, // Hide format change button
titleCentered: true,
titleTextStyle: TextStyle(fontSize: 17.0, fontWeight: FontWeight.bold),
leftChevronIcon: Icon(Icons.chevron_left, color: Colors.blue),
rightChevronIcon: Icon(Icons.chevron_right, color: Colors.blue),
),
calendarStyle: CalendarStyle(
selectedDecoration: BoxDecoration(
color: Colors.blueAccent,
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: Colors.blue.withOpacity(0.5),
shape: BoxShape.circle,
),
defaultTextStyle: TextStyle(color: Colors.black87),
weekendTextStyle: TextStyle(color: Colors.redAccent),
outsideDaysVisible: false, // Hide days from previous/next month
),
// ...
),
You can also define custom builders for various elements, giving you pixel-perfect control over how each part of the calendar looks:
TableCalendar(
// ... (previous properties)
calendarBuilders: CalendarBuilders(
selectedBuilder: (context, date, focusedDay) {
return Container(
margin: const EdgeInsets.all(4.0),
alignment: Alignment.center,
decoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
child: Text(
'${date.day}',
style: const TextStyle(color: Colors.white),
),
);
},
todayBuilder: (context, date, focusedDay) {
return Container(
margin: const EdgeInsets.all(4.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.3),
shape: BoxShape.circle,
),
child: Text(
'${date.day}',
style: const TextStyle(color: Colors.black),
),
);
},
markerBuilder: (context, date, events) {
if (events.isNotEmpty) {
return Positioned(
right: 1,
bottom: 1,
child: _buildEventsMarker(events.length),
);
}
return null;
},
),
)
Widget _buildEventsMarker(int count) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.deepOrange[300],
),
width: 16.0,
height: 16.0,
child: Center(
child: Text(
'$count',
style: const TextStyle().copyWith(
color: Colors.white,
fontSize: 12.0,
),
),
),
);
}
Advanced Features: Event Handling
A calendar is often used to display events. table_calendar makes this straightforward with its eventLoader property. First, you'll need a way to store your events, typically a Map<DateTime, List<dynamic>> or similar structure.
// Define a sample event class
class Event {
final String title;
const Event(this.title);
@override
String toString() => title;
}
// In your _CalendarPageState class:
late final ValueNotifier<List<Event>> _selectedEvents;
final Map<DateTime, List<Event>> kEvents = {
DateTime.utc(2024, 1, 20): [const Event('Flutter Meetup')],
DateTime.utc(2024, 1, 22): [const Event('Project Deadline'), const Event('Client Call')],
DateTime.utc(2024, 1, 25): [const Event('Team Lunch')],
DateTime.utc(2024, 2, 1): [const Event('Sprint Review')],
};
@override
void initState() {
super.initState();
_selectedDay = _focusedDay;
_selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
}
@override
void dispose() {
_selectedEvents.dispose();
super.dispose();
}
List<Event> _getEventsForDay(DateTime day) {
return kEvents[day] ?? [];
}
// Update onDaySelected to handle events
onDaySelected: (selectedDay, focusedDay) {
if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
_selectedEvents.value = _getEventsForDay(selectedDay);
}
},
// Add eventLoader to TableCalendar
TableCalendar(
// ...
eventLoader: _getEventsForDay,
// ...
),
// Display selected events in your UI (e.g., below the calendar)
Expanded(
child: ValueListenableBuilder<List<Event>>(
valueListenable: _selectedEvents,
builder: (context, value, _) {
return ListView.builder(
itemCount: value.length,
itemBuilder: (context, 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('${value[index]}'),
title: Text('${value[index]}'),
),
);
},
);
},
),
),
Here, we introduce a ValueNotifier to efficiently update the list of events displayed below the calendar without rebuilding the entire widget tree. The _getEventsForDay function retrieves events for a given day from our kEvents map.
Complete Example
Combining all the concepts discussed, here is a more comprehensive example of an interactive calendar widget:
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
// Sample Event class
class Event {
final String title;
const Event(this.title);
@override
String toString() => title;
}
// Sample events data
final Map<DateTime, List<Event>> kEvents = {
DateTime.utc(2024, 1, 20): [const Event('Flutter Meetup')],
DateTime.utc(2024, 1, 22): [const Event('Project Deadline'), const Event('Client Call')],
DateTime.utc(2024, 1, 25): [const Event('Team Lunch')],
DateTime.utc(2024, 2, 1): [const Event('Sprint Review')],
DateTime.utc(2024, 2, 10): [const Event('Birthday Party')],
DateTime.utc(2024, 2, 15): [const Event('Travel Planning')],
DateTime.utc(2024, 3, 5): [const Event('Conference')],
};
class CalendarPage extends StatefulWidget {
const CalendarPage({super.key});
@override
State<CalendarPage> createState() => _CalendarPageState();
}
class _CalendarPageState extends State<CalendarPage> {
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime _focusedDay = DateTime.now();
DateTime? _selectedDay;
late final ValueNotifier<List<Event>> _selectedEvents;
@override
void initState() {
super.initState();
_selectedDay = _focusedDay;
_selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
}
@override
void dispose() {
_selectedEvents.dispose();
super.dispose();
}
List<Event> _getEventsForDay(DateTime day) {
// Returns a list of [Event] for the given [day].
return kEvents[day] ?? [];
}
void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay;
});
_selectedEvents.value = _getEventsForDay(selectedDay);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Interactive Calendar'),
backgroundColor: Colors.blueAccent,
),
body: Column(
children: [
TableCalendar<Event>(
firstDay: DateTime.utc(2010, 10, 16),
lastDay: DateTime.utc(2030, 3, 14),
focusedDay: _focusedDay,
selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
calendarFormat: _calendarFormat,
eventLoader: _getEventsForDay,
startingDayOfWeek: StartingDayOfWeek.monday,
onDaySelected: _onDaySelected,
onFormatChanged: (format) {
if (_calendarFormat != format) {
setState(() {
_calendarFormat = format;
});
}
},
onPageChanged: (focusedDay) {
_focusedDay = focusedDay; // No setState needed
},
headerStyle: const HeaderStyle(
formatButtonVisible: false,
titleCentered: true,
titleTextStyle: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.blueAccent),
leftChevronIcon: Icon(Icons.chevron_left, color: Colors.blueAccent),
rightChevronIcon: Icon(Icons.chevron_right, color: Colors.blueAccent),
),
calendarStyle: CalendarStyle(
outsideDaysVisible: false,
weekendTextStyle: const TextStyle(color: Colors.redAccent),
selectedDecoration: const BoxDecoration(
color: Colors.blue,
shape: BoxShape.circle,
),
todayDecoration: BoxDecoration(
color: Colors.blue.withOpacity(0.5),
shape: BoxShape.circle,
),
markerDecoration: const BoxDecoration(
color: Colors.deepOrange,
shape: BoxShape.circle,
),
),
calendarBuilders: CalendarBuilders(
markerBuilder: (context, date, events) {
if (events.isNotEmpty) {
return Positioned(
right: 1,
bottom: 1,
child: _buildEventsMarker(events.length),
);
}
return null;
},
),
),
const SizedBox(height: 8.0),
Expanded(
child: ValueListenableBuilder<List<Event>>(
valueListenable: _selectedEvents,
builder: (context, value, _) {
return ListView.builder(
itemCount: value.length,
itemBuilder: (context, index) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 12.0,
vertical: 4.0,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.blueAccent),
borderRadius: BorderRadius.circular(12.0),
),
child: ListTile(
onTap: () => print('Tapped event: ${value[index]}'),
title: Text(value[index].title),
),
);
},
);
},
),
),
],
),
);
}
Widget _buildEventsMarker(int count) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.deepOrange[300],
),
width: 16.0,
height: 16.0,
child: Center(
child: Text(
'$count',
style: const TextStyle().copyWith(
color: Colors.white,
fontSize: 12.0,
),
),
),
);
}
}
To run this example, replace the content of your main.dart file with a simple MaterialApp that uses CalendarPage:
import 'package:flutter/material.dart';
import 'package:your_app_name/calendar_page.dart'; // Adjust import path
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Calendar Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const CalendarPage(),
);
}
}
Remember to create a new file named calendar_page.dart (or similar) for the CalendarPage widget and adjust the import path accordingly.
Conclusion
Creating an interactive calendar widget in Flutter is straightforward with the help of the table_calendar package. This powerful library offers excellent flexibility for both functionality and appearance, allowing developers to build calendars that seamlessly integrate into any application. By mastering date selection, event handling, and customization, you can provide users with a rich and engaging way to manage their schedules and interact with time-based data.