Custom Calendar in Flutter Application

Custom Calendar in Flutter Application

Most applications need to work with dates. We can use the date picker from Flutter or custom packages from other developers. These options might have design limits, and we have to follow their design. But we can create a calendar based on our design and use it to make a DatePicker too. In this article, I will create a calendar with a custom design.

Let's start with dividing the article contents.

  1. Requirements for calendar

  2. Generating dates for the month

  3. Building UI for the days of a week

  4. Building UI for each cell

  5. Building UI for a single month

  6. Showing events in the calendar

  7. Generating calendar blocks for a whole year

  8. DateTime Extension

  9. Conclusion

1. Requirements for a calendar

Different countries use different calendars. The most commonly used one is the AD Calendar, and we are going to create an AD Calendar.

The calendar will have 12 months. Each month has 28 to 31 days depending on each month. We need to add a row defining the seven-week days. Also, we may need to show either 5 or 6 rows of dates based on the first weekday of the month. The starting day of the week can be different based on countries. We need to add the dates of the previous month and the next month too.

2. Generating dates for the month

Let's start by defining the required variables and then generate all the dates for the month. Each month block can contain the previous month's dates and next month dates as well depending on the weekday of first date of the month.

List<DateTime> cells = [];
var month = DateTime.now();
var totalDaysInMonth = DateUtils.getDaysInMonth(month.year, month.month);

var firstDayDt = DateTime(month.year, month.month, 1);
var previousMonthDt = firstDayDt.subtract(const Duration(days: 1));
var nextMonthDt = DateTime(month.year, month.month, totalDaysInMonth)
    .add(const Duration(days: 1));

var firstDayOfWeek = firstDayDt.weekday;
var previousMonthDays =
    DateUtils.getDaysInMonth(previousMonthDt.year, previousMonthDt.month);

Here, I am creating the calendar based on starting day as Monday. Let's generate the previous month's date cells.

// previous month days
var previousMonthCells = List.generate(
    firstDayOfWeek - 1,
    (index) => DateTime(previousMonthDt.year, previousMonthDt.month,
        previousMonthDays - index));
cells.addAll(previousMonthCells.reversed);

For current month date cells.

// current month days
var currentMonthCells = List.generate(totalDaysInMonth,
    (index) => DateTime(month.year, month.month, index + 1));
cells.addAll(currentMonthCells);

Now, generating next month's cells and adding all to the variable cells . Here 6th rows are created if 5 rows are not enough to show all dates for the current month.

// next month days
var remainingCellCount = 35 - cells.length;
if (cells.length > 35) {
  remainingCellCount = 42 - cells.length;
}
var nextMonthCells = List.generate(remainingCellCount,
    (index) => DateTime(nextMonthDt.year, nextMonthDt.month, index + 1));
cells.addAll(nextMonthCells);
_monthCellsMap[month.month] = cells;

You can create a function and add all the code blocks to the single function generateMonthCells() .

3. Building UI for the days of a week

Let's create the weekdays row starting with Monday as the start of the week. Most of the IT companies work 5 days a week and others can have a different shift. Here, I am representing Saturday and Sunday in two different colors through the function _getTitleColor().

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: 28,
      child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
              .mapIndexed(
                (i, item) => Expanded(
                  child: Text(
                    item,
                    style: TextStyle(
                      fontSize: 14,
                      fontWeight: FontWeight.w700,
                      color: _getTitleColor(i),
                    ),
                    textAlign: TextAlign.center,
                  ),
                ),
              )
              .toList()),
    );
  }

  _getTitleColor(int index) {
    if (index == 5) {
      return Colors.blueAccent;
    }

    if (index == 6) {
      return const Color(0xFFFF8181);
    }

    return const Color(0xAD1A1642);
  }
}

This will create the following week days in a horizontal view.

4. Building UI for each cell

Let's create the user interface for each month cell.

var item = cells[index];
var isToday = item.isToday;
var isSameMonth = item.month == month.month;
var isHoliday = holidays.contains(item.dateDash);

Color? bgColor;
Color? textColor;

if (item.isSaturday) {
  textColor = Colors.blueAccent;
}

if (item.isSunday) {
  textColor = Colors.red;
}

if (item.isToday && isHoliday) {
  bgColor = Colors.red;
  textColor = Colors.white;
} else if (isToday) {
  bgColor = Colors.blueAccent;
  textColor = Colors.white;
} else if (isHoliday) {
  bgColor = const Color(0xFFFFEFEF);
  textColor = Colors.red;
}

return Card(
                  color: bgColor,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: InkWell(
                    onTap: isHoliday
                        ? () {
                         }
                        : null,
                    borderRadius: BorderRadius.circular(10),
                    child: Opacity(
                      opacity: isSameMonth ? 1 : 0.3,
                      child: Stack(
                        children: [
                          Center(
                              child: Text(
                            item.day.toString(),
                            style: TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.w600,
                                color: textColor),
                          )),
                        ],
                      ),
                    ),
                  ),
                );

Here, each cell is generated on cell iteration, and styles are applied to distinguish each cell. It will generate the following output for the first week of February 2024.

It has three dates (27,28,29) from January which seem inactive for the current month. Feb 1-4 have different colors based on the working days and weekends.

February end week will look like below. The 3 days from March seem inactive.

5. Building UI for a single month

Let's write the code to generate the whole month's dates using the GridView. shrinkWrap is set to true to disable scrolling inside the GridView. cells for the month is passed and each cell is generated based on the above code for creating each cell.

GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 7,
                childAspectRatio: 0.9,
                mainAxisSpacing: 2,
                crossAxisSpacing: 2,
              ),
              itemCount: cells.length,
              itemBuilder: (context, index) {
                ....
            }
}

I had passed the holidaysMap for showing holiday events. I will explain that later in another section below. The full code for generating the monthly view is below. Header widget is also used to show the month name above the dates block.


class CalendarBlock extends StatelessWidget {
  const CalendarBlock(
    this.month,
    this.cells,
    this.holidaysMap, {
    required this.today,
    this.header,
    this.isJapanese = true,
    super.key,
  });

  final DateTime today;
  final DateTime month;
  final List<DateTime> cells;
  final Map<String, String> holidaysMap;
  final Widget? header;
  final bool isJapanese;

  @override
  Widget build(BuildContext context) {
    var holidays = holidaysMap.keys.toList();
    return Container(
      padding: const EdgeInsets.all(4),
      decoration: BoxDecoration(
          color: Colors.white, borderRadius: BorderRadius.circular(14)),
      child: Column(
        children: [
          Align(alignment: Alignment.center, child: header ?? const SizedBox()),
          const _WeekHeaders(),
          GridView.builder(
              shrinkWrap: true,
              physics: const NeverScrollableScrollPhysics(),
              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                crossAxisCount: 7,
                childAspectRatio: 0.9,
                mainAxisSpacing: 2,
                crossAxisSpacing: 2,
              ),
              itemCount: cells.length,
              itemBuilder: (context, index) {
                var item = cells[index];
                var isToday = item.isToday;
                var isSameMonth = item.month == month.month;
                var isHoliday = holidays.contains(item.dateDash);

                Color? bgColor;
                Color? textColor;

                if (item.isSaturday) {
                  textColor = Colors.blueAccent;
                }

                if (item.isSunday) {
                  textColor = Colors.red;
                }

                if (item.isToday && isHoliday) {
                  bgColor = Colors.red;
                  textColor = Colors.white;
                } else if (isToday) {
                  bgColor = Colors.blueAccent;
                  textColor = Colors.white;
                } else if (isHoliday) {
                  bgColor = const Color(0xFFFFEFEF);
                  textColor = Colors.red;
                }

                return Card(
                  color: bgColor,
                  shape: RoundedRectangleBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: InkWell(
                    onTap: isHoliday
                        ? () {
                          }
                        : null,
                    borderRadius: BorderRadius.circular(10),
                    child: Opacity(
                      opacity: isSameMonth ? 1 : 0.3,
                      child: Stack(
                        children: [
                          Center(
                              child: Text(
                            item.day.toString(),
                            style: TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.w600,
                                color: textColor),
                          )),
                        ],
                      ),
                    ),
                  ),
                );
              })
        ],
      ),
    );
  }
}

It will produce the following output for the provided cells.

6. Showing events in the calendar

We may also need to show events and public holidays on the calendar. These can be fetched remotely and used inside the application. Here, I am using US holidays for the year 2024.

final List<String> _holidays = [];
final Map<String, String> _holidaysMap = {};

setHolidays() {
    // US United States / Public holidays (2024)
    _holidaysMap.addAll({
      "2024-01-01": "New Year's Day",
      "2024-01-15": "Martin Luther King Jr. Day",
      "2024-02-19": "Presidents' Day",
      "2024-05-27": "Memorial Day",
      "2024-06-19": "Juneteenth National Independence Day",
      "2024-07-04": "Independence Day",
      "2024-09-02": "Labor Day",
      "2024-10-14": "Columbus Day",
      "2024-11-11": "Veterans Day",
      "2024-11-28": "Thanksgiving Day",
      "2024-12-25": "Christmas Day",
    });
    _holidays.addAll(_holidaysMap.keys.toList());
  }

I am using ModalBottomSheet Dialog to show the clicked holiday information.

InkWell(
  onTap: isHoliday
      ? () {
          var holidayText = holidaysMap[item.dateDash] ??
              "This day is Holiday";
          showModalBottomSheet(
              context: context,
              backgroundColor: Colors.white,
              builder: (_) {
                return Padding(
                    padding: const EdgeInsets.all(20),
                    child: Column(
                      crossAxisAlignment:
                          CrossAxisAlignment.stretch,
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        Text(
                          "${item.dateDashEEEE} (Holiday)",
                          style: Theme.of(context)
                              .textTheme
                              .headlineSmall,
                        ),
                        vs10x2,
                        Container(
                          padding: const EdgeInsets.all(20),
                          decoration: BoxDecoration(
                              color: const Color(0xFFffcc17)
                                  .withOpacity(0.1),
                              borderRadius:
                                  BorderRadius.circular(4)),
                          child: Text(holidayText),
                        ),
                        vs10x2,
                        FilledButton(
                          onPressed: () {
                            Navigator.pop(context);
                          },
                          child: const Text("Close"),
                        ),
                        vs10x2,
                      ],
                    ));
              });
        }
      : null,
  borderRadius: BorderRadius.circular(10),
)

On clicking the holiday date it will show information in the below bottom sheet dialog.

7. Generating calendar blocks for a whole year

To generate the calendar for a year, I had to generate the month cells for each place on a map variable, and later use it to generate the ui for the whole year.

final DateTime _today = DateTime.now();
final Map<int, List<DateTime>> _monthCellsMap = {};

  @override
  void initState() {
    setHolidays();
    List.generate(12, (index) {
      generateMonthCells(DateTime(_today.year, index + 1, 1));
    });
    super.initState();
  }

  generateMonthCells(DateTime month) async {
    List<DateTime> cells = [];
    var totalDaysInMonth = DateUtils.getDaysInMonth(month.year, month.month);

    var firstDayDt = DateTime(month.year, month.month, 1);
    var previousMonthDt = firstDayDt.subtract(const Duration(days: 1));
    var nextMonthDt = DateTime(month.year, month.month, totalDaysInMonth)
        .add(const Duration(days: 1));

    var firstDayOfWeek = firstDayDt.weekday;
    var previousMonthDays =
        DateUtils.getDaysInMonth(previousMonthDt.year, previousMonthDt.month);

    // previous month days
    var previousMonthCells = List.generate(
        firstDayOfWeek - 1,
        (index) => DateTime(previousMonthDt.year, previousMonthDt.month,
            previousMonthDays - index));
    cells.addAll(previousMonthCells.reversed);

    // current month days
    var currentMonthCells = List.generate(totalDaysInMonth,
        (index) => DateTime(month.year, month.month, index + 1));
    cells.addAll(currentMonthCells);

    // next month days
    var remainingCellCount = 35 - cells.length;
    if (cells.length > 35) {
      remainingCellCount = 42 - cells.length;
    }
    var nextMonthCells = List.generate(remainingCellCount,
        (index) => DateTime(nextMonthDt.year, nextMonthDt.month, index + 1));
    cells.addAll(nextMonthCells);
    _monthCellsMap[month.month] = cells;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("Custom Yearly Calendar"),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Column(
            children: [
              ...List.generate(12, (index) {
                var calendarMonth = DateTime(_today.year, index + 1, 1);
                var cells = _monthCellsMap[index + 1] ?? [];
                return Card(
                  color: Colors.white,
                  margin: const EdgeInsets.only(bottom: 20),
                  shape: RoundedRectangleBorder(
                      borderRadius: BorderRadius.circular(10)),
                  child: CalendarBlock(
                    calendarMonth,
                    cells,
                    _holidaysMap,
                    today: _today,
                    isJapanese: true,
                    header: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                        calendarMonth.calendarTitle,
                        style: Theme.of(context).textTheme.titleLarge,
                      ),
                    ),
                  ),
                );
              }),
            ],
          ),
        ),
      ),
    );
  }
}

8. DateTime Extension

We can use extensions for any classes or enums in Dart to facilitate the common use cases. The extension function used for DateTime is below.

extension DateTimeExt on DateTime {
  toFormat([String? newPattern, String? locale]) {
    return DateFormat(newPattern, locale).format(this);
  }

  String get dateDash => toFormat("yyyy-MM-dd");

  String get dateDashTime => toFormat("yyyy-MM-dd HH:mm");

  String get dateDashEEEE => toFormat("yyyy-MM-dd EEEE");

  String get monthName => toFormat("MMMM");

  String get calendarTitle => toFormat("MMMM yyyy");

  bool get isSaturday => weekday == 6;

  bool get isSunday => weekday == 7;

  bool get isToday {
    var today = DateTime.now();
    return year == today.year && month == today.month && day == today.day;
  }
}

Conclusion

Creating a custom calendar in Flutter allows you to tailor the design and functionality to meet specific requirements, providing greater flexibility than using predefined date pickers. By following the steps outlined in this article, you can generate a fully functional calendar with custom UI elements, handle events and holidays, and extend functionality using Dart extensions. This approach not only enhances the user experience but also demonstrates the powerful capabilities of Flutter for building complex and customized interfaces. For a complete implementation, you can refer to the provided DartPad link.

The full codebase is available on the following dartpad link.
https://dartpad.dev/?id=268115b8531867da9c567b67d8e0d362