Como criar um calendário personalizado no Flutter

Tempo de leitura: 13 minutes

O calendário que usamos evoluiu ao longo dos anos. De um calendário manuscrito a um calendário impresso, agora todos temos em mãos um calendário digital extremamente personalizável e que nos lembra de nossos eventos no momento exato em que queremos um lembrete.

Veremos como podemos construir e personalizar o widget de calendário no Flutter para que possamos fornecer essa experiência aos nossos usuários.

Embora o Flutter forneça um widget de calendário na forma de um seletor de data e hora que oferece cores, fontes e uso personalizáveis, faltam alguns recursos. Você pode usá-lo para escolher uma data e hora (ou ambos) e adicioná-lo ao seu aplicativo, mas precisa ser combinado com um botão e um espaço reservado onde a data ou hora escolhida pode ser salva.

Então, vou começar com o calendário nativo fornecido pela arquitetura Flutter e, em seguida, passar para o TableCalendar, o widget de calendário mais popular no pub.dev. Existem também muitos outros widgets de calendário populares que você pode usar, mas para este tutorial, abordaremos um em detalhes.

 

Widget de calendário Flutter (seletor de data e seletor de hora)

Para explicar o widget com mais detalhes, criei um aplicativo de tela única para reuniões online. Os usuários podem inserir o nome e o link da reunião e, em seguida, escolher uma data e hora.

Primeiro, vamos examinar o construtor padrão showDatePicker:

showDatePicker({
// it requires a context
  required BuildContext context,  
// when datePicker is displayed, it will show month of the current date
  required DateTime initialDate,  
// earliest possible date to be displayed (eg: 2000)
  required DateTime firstDate,
// latest allowed date to be displayed (eg: 2050)
  required DateTime lastDate,
// it represents TODAY and it will be highlighted
  DateTime? currentDate,
 // either by input or selected, defaults to calendar mode.
  DatePickerEntryMode initialEntryMode = DatePickerEntryMode.calendar or input,
// restricts user to select date from range to dates.
  SelectableDayPredicate? selectableDayPredicate,
// text that is displayed at the top of the datePicker
  String? helpText,
// text that is displayed on cancel button
  String? cancelText,
// text that is displayed on confirm button
  String? confirmText,
// use builder function to customise the datePicker  
  TransitionBuilder? Builder,
// option to display datePicker in year or day mode. Defaults to day
  DatePickerMode initialDatePickerMode = DatePickerMode.day or year,
// error message displayed when user hasn't entered date in proper format
  String? errorFormatText,
// error message displayed when date is not selectable
  String? errorInvalidText,
// hint message displayed to prompt user to enter date according to the format mentioned (eg: dd/mm/yyyy)
  String? fieldHintText,
// label message displayed for what the user is entering date for (eg: birthdate)
  String? fieldLabelText,
})

Em relação ao construtor padrão acima, você pode consultar a imagem abaixo onde apontei algumas propriedades importantes que podem ser personalizadas de acordo com suas necessidades.

Como funciona?

Não vou postar todo o código aqui, mas apenas mostrar a implementação dele e explicá-lo. O restante do código para showDatePicker pode ser encontrado aqui para sua experimentação.

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';


const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: FlutterDatePickerExample());
  }
}

class FlutterDatePickerExample extends StatelessWidget {
  final ValueNotifier<DateTime?> dateSub = ValueNotifier(null);
  final ValueNotifier<DateTime?> longDateSub = ValueNotifier(null);
  final ValueNotifier<TimeOfDay?> timeSub = ValueNotifier(null);
  final ValueNotifier<TimeOfDay?> timeSubShort = ValueNotifier(null);
  final TextEditingController meetingName = TextEditingController();
  final TextEditingController meetingLink = TextEditingController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Vanilla Calendar Flutter'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(14.0),
        child: SingleChildScrollView(
        child:Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              ' Create Meeting',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 24.0),
            ),
            const SizedBox(
              height: 20,
            ),
            buildTextField(controller: meetingName, hint: 'Enter Meeting Name'),
            const SizedBox(
              height: 20,
            ),
            buildTextField(controller: meetingLink, hint: 'Enter Meeting Link'),
            const SizedBox(
              height: 10,
            ),
            const Text(
              ' Short Date',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<DateTime?>(
                valueListenable: dateSub,
                builder: (context, dateVal, child) {
                  return InkWell(
                      onTap: () async {
                        DateTime? date = await showDatePicker(
                            context: context,
                            initialDate: DateTime.now(),
                            firstDate: DateTime.now(),
                            lastDate: DateTime(2050),
                            currentDate: DateTime.now(),
                            initialEntryMode: DatePickerEntryMode.calendar,
                            initialDatePickerMode: DatePickerMode.day,
                            builder: (context, child) {
                              return Theme(
                                data: Theme.of(context).copyWith(
                                    colorScheme: const ColorScheme.light(
                                      primary: Colors.blueGrey,
                                      onSurface: AppColors.blackCoffee,
                                    )
                                ),
                                child: child!,
                              );
                            });
                        dateSub.value = date;
                      },
                      child: buildDateTimePicker(
                          dateVal != null ? convertDate(dateVal) : ''));
                }),
            const SizedBox(
              height: 10,
            ),
            const Text(
              ' 12H Format Time',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<TimeOfDay?>(
                valueListenable: timeSubShort,
                builder: (context, timeVal, child) {
                  return InkWell(
                      onTap: () async {
                        TimeOfDay? time = await showTimePicker(
                          context: context,
                          builder: (context, child) {
                            return Theme(
                              data: Theme.of(context)
                              child: child!,
                            );
                          },
                          initialTime: TimeOfDay.now(),
                        );
                        timeSubShort.value = time;
                      },
                      child: buildDateTimePicker(timeVal != null
                          ? convertTime(timeVal)
                          : ''));
                }),
            const SizedBox(
              height: 20.0,
            ),
            const Text(
              ' Long Date',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<DateTime?>(
                valueListenable: longDateSub,
                builder: (context, dateVal, child) {
                  return InkWell(
                      onTap: () async {
                        DateTime? date = await showDatePicker(
                            context: context,
                            initialDate: DateTime.now(),
                            firstDate: DateTime.now(),
                            lastDate: DateTime(2050),
                            builder: (context, child) {
                              return Theme(
                                data: Theme.of(context),
                                child: child!,
                              );
                            });
                        longDateSub.value = date;
                      },
                      child: buildDateTimePicker(
                          dateVal != null ? longDate(dateVal) : ''));
                }),
            const SizedBox(
              height: 10,
            ),
            const Text(
              ' 24H Format Time',
              textAlign: TextAlign.left,
              style: TextStyle(fontSize: 18.0),
            ),
            ValueListenableBuilder<TimeOfDay?>(
                valueListenable: timeSub,
                builder: (context, timeVal, child) {
                  return InkWell(
                      onTap: () async {
                        TimeOfDay? time = await showTimePicker(
                          context: context,
                          builder: (context, child) {
                            return MediaQuery(
                              data: MediaQuery.of(context).copyWith(
                                  alwaysUse24HourFormat: true),
                              child: child!,
                            );
                          },
                          initialTime: TimeOfDay.now(),
                        );
                        timeSub.value = time;
                      },
                      child: buildDateTimePicker(timeVal != null
                          ? timeVal.format(context)
                          : ''));
                }),
            const SizedBox(height: 20.0,),
            ElevatedButton(onPressed: () {
              ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
                content: Text('Meeting Created'),
                duration: Duration(seconds: 5),));
            }, child: const Text('SUBMIT')),
          ],
        ),
        ),
        
      ),
    );
  }

  String convertDate(DateTime dateTime) {
    return DateFormat('dd/MM/yyyy').format(dateTime);
  }

  String longDate(DateTime dateTime) {
    return DateFormat('EEE, MMM d, yyy').format(dateTime);
  }

  String convertTime(TimeOfDay timeOfDay) {
    DateTime tempDate = DateFormat('hh:mm').parse(
        timeOfDay.hour.toString() + ':' + timeOfDay.minute.toString());
    var dateFormat = DateFormat('h:mm a');
    return dateFormat.format(tempDate);
  }


  Widget buildDateTimePicker(String data) {
    return ListTile(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(10.0),
        side: const BorderSide(color: AppColors.eggPlant, width: 1.5),
      ),
      title: Text(data),
      trailing: const Icon(
        Icons.calendar_today,
        color: AppColors.eggPlant,
      ),
    );
  }

  Widget buildTextField(
      {String? hint, required TextEditingController controller}) {
    return TextField(
      controller: controller,
      textCapitalization: TextCapitalization.words,
      decoration: InputDecoration(
        labelText: hint ?? '',
        focusedBorder: OutlineInputBorder(
          borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
          borderRadius: BorderRadius.circular(
            10.0,
          ),
        ),
        enabledBorder: OutlineInputBorder(
          borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
          borderRadius: BorderRadius.circular(
            10.0,
          ),
        ),
      ),
    );
  }
 
}
 class AppColors {
  AppColors._();

  static const Color blackCoffee = Color(0xFF352d39);
  static const Color eggPlant = Color(0xFF6d435a);
  static const Color celeste = Color(0xFFb1ede8);
  static const Color babyPowder = Color(0xFFFFFcF9);
  static const Color ultraRed = Color(0xFFFF6978);
}

 

Etapa 1: Implementando um ValueNotifier

Eu implementei um ValueNotifier que manterá a data no campo de texto.

final ValueNotifier<DateTime?> dateSub = ValueNotifier(null);

Etapa 2: criando uma caixa de diálogo datePicker

Com ValueListenerBuilder e uma instância de DateTime, e com a ajuda do widget InkWell, quando clicarmos no textField, uma caixa de diálogo datePicker aparecerá. Quando um usuário tocar na data desejada, aparecerá no textField:

ValueListenableBuilder<DateTime?>(
   valueListenable: dateSub,
   builder: (context, dateVal, child) {
     return InkWell(
         onTap: () async {
           DateTime? date = await showDatePicker(
               context: context,
               initialDate: DateTime.now(),
               firstDate: DateTime.now(),
               lastDate: DateTime(2050),
               currentDate: DateTime.now(),
               initialEntryMode: DatePickerEntryMode.calendar,
               initialDatePickerMode: DatePickerMode.day,
               builder: (context, child) {
                 return Theme(
                   data: Theme.of(context).copyWith(
                       colorScheme:  ColorScheme.fromSwatch(
                         primarySwatch: Colors.blueGrey,
                         accentColor: AppColors.blackCoffee,
                         backgroundColor: Colors.lightBlue,
                         cardColor: Colors.white,
                       )
                   ),
                   child: child!,
                 );
               });
           dateSub.value = date;
         },
         child: buildDateTimePicker(
             dateVal != null ? convertDate(dateVal) : ''));
   }),

buildDateTimePicker nada mais é que um listTile com uma borda personalizada e um ícone de calendário como um ícone à direita:

Widget buildDateTimePicker(String data) {
 return ListTile(
   shape: RoundedRectangleBorder(
     borderRadius: BorderRadius.circular(10.0),
     side: const BorderSide(color: AppColors.eggPlant, width: 1.5),
   ),
   title: Text(data),
   trailing: const Icon(
     Icons.calendar_today,
     color: AppColors.eggPlant,
   ),
 );
}

Também temos um método string para converter a data para o formato desejado:

String convertDate(DateTime dateTime) {
 return DateFormat('dd/MM/yyyy').format(dateTime);
}

É assim que vai ficar quando o código for implementado:

Agora, vamos voltar ao TableCalendar que discuti antes, como vamos implementá-lo e como vamos personalizá-lo para atender às demandas do aplicativo.

Existem várias possibilidades de personalização e discuti-las ultrapassaria o escopo deste artigo. Portanto, tentarei ser o mais específico possível e abordar apenas as partes mais significativas. Claro, existem implementações de código que experimentei pessoalmente, bem como imagens para referência.

 

TableCalendar

A instalação é bastante direta: você precisa copiar e colar a dependência em seu arquivo pubspec.yaml para table_calendar daqui.

A versão mais recente é:

table_calendar: ^3.0.9

Agora, vou dividir seu construtor em três partes:

  1. Configurando o widget TableCalendar
  2. Estilizando o calendário para as necessidades do seu aplicativo
  3. Adicionando eventos ao calendário

Isso é para que você possa entender o código facilmente e também saber como implementá-lo com sucesso.

Passo 1: Configurando o widget TableCalendar

Usei SingleChildScrollView como meu widget pai e, em seguida, adicionei um widget Card dentro de um widget Column para dar uma pequena elevação ao calendário. Em seguida, adicionei o widget TableCalendar dentro do widget Card como seu filho:

SingleChildScrollView(
 child: Column(
   children: [
     Card(
       margin: const EdgeInsets.all(8.0),
       elevation: 5.0,
       shape: const RoundedRectangleBorder(
         borderRadius: BorderRadius.all(
           Radius.circular(10),
         ),
         side: BorderSide( color: AppColors.blackCoffee, width: 2.0),
       ),
       child: TableCalendar(
          // today's date
         focusedDay: _focusedCalendarDate,
         // earliest possible date
         firstDay: _initialCalendarDate,
         // latest allowed date
         lastDay: _lastCalendarDate, 
         // default view when displayed
         calendarFormat: CalendarFormat.month, 
         // default is Saturday & Sunday but can be set to any day.
         // instead of day, a number can be mentioned as well.
         weekendDays: const [DateTime.sunday, 6],
         // default is Sunday but can be changed according to locale
         startingDayOfWeek: StartingDayOfWeek.monday,
        // height between the day row and 1st date row, default is 16.0
         daysOfWeekHeight: 40.0,
         // height between the date rows, default is 52.0
         rowHeight: 60.0,

O código acima está configurando o calendário que ficará na tela do celular com alguns valores padrão e algumas customizações de acordo com a localidade. Adicionei comentários antes de cada propriedade para entender o que ela faz.

Sei que a explicação já está dada no arquivo de classe do widget TableCalendar, mas às vezes é mais fácil entender propriedade em termos mais simples. Eu tenho o hábito de ler tudo, entender, e depois tento simplificar para meus leitores para que eles não precisem passar por todas as linhas antes de implementar o código.

Passo 2: Estilizando o TableCalendar

Ok, então há novamente 3 partes no estilo do calendário de mesa. O primeiro é o cabeçalho onde temos o nome do mês e um botão para alternar entre a visualização da semana e a visualização do mês. As setas esquerda e direita rolam entre os meses.

De acordo com o tema do aplicativo, você pode personalizar tudo para que a aparência do calendário, basicamente toda a IU do calendário, corresponda à IU do seu aplicativo.

Dividindo o código em 3 partes novamente:

headerStyle

// Calendar Header Styling
headerStyle: const HeaderStyle(
 titleTextStyle:
     TextStyle(color: AppColors.babyPowder, fontSize: 20.0),
 decoration: BoxDecoration(
     color: AppColors.eggPlant,
     borderRadius: BorderRadius.only(
         topLeft: Radius.circular(10),
         topRight: Radius.circular(10))),
 formatButtonTextStyle:
     TextStyle(color: AppColors.ultraRed, fontSize: 16.0),
 formatButtonDecoration: BoxDecoration(
   color: AppColors.babyPowder,
   borderRadius: BorderRadius.all(
     Radius.circular(5.0),
   ), ),
 leftChevronIcon: Icon(
   Icons.chevron_left,
   color: AppColors.babyPowder,
   size: 28,
 ),
 rightChevronIcon: Icon(
   Icons.chevron_right,
   color: AppColors.babyPowder,
   size: 28,
 ),
),

Dias de estilo abaixo do header

Aqui você pode definir uma cor diferente para fins de semana, dias de semana e também para feriados, se tiver definido algum:

// Calendar Days Styling
daysOfWeekStyle: const DaysOfWeekStyle(
 // Weekend days color (Sat,Sun)
 weekendStyle: TextStyle(color: AppColors.ultraRed),
),

No código acima, adicionei cores aos dias de fim de semana que defini inicialmente quando implementei o widget TableCalendar.

 

Estilizando as datas

Aqui você pode adicionar cor às datas específicas de fim de semana ou feriados. Além disso, a cor destacada para a data atual e a data selecionada podem ser personalizadas.

// Calendar Dates styling
calendarStyle: const CalendarStyle(
 // Weekend dates color (Sat & Sun Column)
 weekendTextStyle: TextStyle(color: AppColors.ultraRed),
 // highlighted color for today
 todayDecoration: BoxDecoration(
   color: AppColors.eggPlant,
   shape: BoxShape.circle,
 ),
 // highlighted color for selected day
 selectedDecoration: BoxDecoration(
   color: AppColors.blackCoffee,
   shape: BoxShape.circle,
 ),
),

O próximo bloco de código é da documentação oficial fornecida pelo TableCalender. É a maneira padrão de implementar o dia selecionado. Este código destaca a data atual e também a data selecionada com base nas cores personalizadas acima. Não há melhor maneira de fazer isso, e é aconselhado pelo TableCalendar:

selectedDayPredicate: (currentSelectedDate) {
 // as per the documentation 'selectedDayPredicate' needs to determine current selected day.
 return (isSameDay(
     _selectedCalendarDate!, currentSelectedDate));
},
onDaySelected: (selectedDay, focusedDay) {
 // as per the documentation
 if (!isSameDay(_selectedCalendarDate, selectedDay)) {
   setState(() {
     _selectedCalendarDate = selectedDay;
     _focusedCalendarDate = focusedDay;
   });
 }
},

Passo 3: Adicionando eventos ao TableCalendar

Assim, terminamos de inicializar o TableCalendar e o estilizamos para corresponder à nossa IU. A única coisa que resta é adicionar eventos ao nosso calendário, o que é um recurso vital. Sem ele, nosso calendário é simplesmente uma cópia impressa que mantemos em nossas casas ou em nossas geladeiras.

No entanto, muitos de nós tendem a colar um post-it no calendário para indicar os principais eventos ao longo do mês, da semana ou até mesmo do dia. Em nossos telefones celulares, podemos adicionar lembretes ou eventos ao nosso aplicativo de calendário padrão.

Eu criei uma classe de modelo chamada MyEvents e inicializei duas variáveis String eventTitle e eventDescp (descrição):

class MyEvents {
 final String eventTitle;
 final String eventDescp;

 MyEvents({required this.eventTitle, required this.eventDescp});

 @override
 String toString() => eventTitle;
}

Em nosso arquivo Dart CustomCalendarTable, adicionei dois TextEditingControllers, um Map e um método onde iremos manter nossa lista de eventos e aplicá-la à propriedade eventLoader dentro de TableCalandar:

final titleController = TextEditingController();
final descpController = TextEditingController();

late Map<DateTime, List<MyEvents>> mySelectedEvents;

@override
void initState() {
 selectedCalendarDate = _focusedCalendarDate;
 mySelectedEvents = {};
 super.initState();
}

@override
void dispose() {
 titleController.dispose();
 descpController.dispose();
 super.dispose();
}

List<MyEvents> _listOfDayEvents(DateTime dateTime) {
 return mySelectedEvents[dateTime] ?? [];
}

Em seguida, adicionei um botão fab ao nosso Scaffold e ao clicar no botão fab, um AlertDialog aparecerá, onde o usuário estará inserindo o título do evento e a descrição do evento.

Após clicar no botão Add dentro do AlertDialog, um evento será adicionado no calendário e um pequeno ponto colorido será visto na data em que o evento foi adicionado.

Também adicionei um SnackBar caso o usuário não insira nada no campo de texto do título ou no campo de texto da descrição. Um SnackBar aparecerá com uma mensagem para inserir o título e a descrição.

Se o usuário digitou o título e a descrição, no método setState ele está verificando se a lista de eventos selecionados não é nula e então estamos adicionando o título e a descrição na classe do modelo MyEvents e criando uma lista de MyEvents.

Assim que um evento é adicionado, limpamos os Controllers e fechamos o AlertDialog:

_showAddEventDialog() async {
 await showDialog(
     context: context,
     builder: (context) => AlertDialog(
           title: const Text('New Event'),
           content: Column(
             crossAxisAlignment: CrossAxisAlignment.stretch,
             mainAxisSize: MainAxisSize.min,
             children: [
               buildTextField(
                   controller: titleController, hint: 'Enter Title'),
               const SizedBox(
                 height: 20.0,
               ),
               buildTextField(
                   controller: descpController, hint: 'Enter Description'),
             ],           ),
           actions: [
             TextButton(
               onPressed: () => Navigator.pop(context),
               child: const Text('Cancel'),),
             TextButton(
               onPressed: () {
                 if (titleController.text.isEmpty &&
                     descpController.text.isEmpty) {
                   ScaffoldMessenger.of(context).showSnackBar(
                     const SnackBar(
                       content: Text('Please enter title & description'),
                       duration: Duration(seconds: 3),
                     ), );
                   //Navigator.pop(context);
                   return;
                 } else {
                   setState(() {
                if (mySelectedEvents[selectedCalendarDate] != null) {
                     mySelectedEvents[selectedCalendarDate]?.add(MyEvents(
                           eventTitle: titleController.text,
                           eventDescp: descpController.text));
                     } else {
                       mySelectedEvents[selectedCalendarDate!] = [
                         MyEvents(
                             eventTitle: titleController.text,
                             eventDescp: descpController.text)
                       ]; } });

                   titleController.clear();
                   descpController.clear();

                   Navigator.pop(context);
                   return;
                 }
               },
               child: const Text('Add'),
             ),
           ],
         ));}

Criei um campo de texto personalizado que inicializei dentro do AlertDialog:

Widget buildTextField(
   {String? hint, required TextEditingController controller}) {
 return TextField(
   controller: controller,
   textCapitalization: TextCapitalization.words,
   decoration: InputDecoration(
     labelText: hint ?? '',
     focusedBorder: OutlineInputBorder(
       borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
       borderRadius: BorderRadius.circular(
         10.0,
       ),
     ),
     enabledBorder: OutlineInputBorder(
       borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
       borderRadius: BorderRadius.circular(
         10.0,
       ),
     ),
   ),
 );
}

Tudo se encaixa quando eu adiciono a propriedade eventLoader que está sob o widget TableCalendar e adiciono o método _listofDayEvents a ela:

// this property needs to be added to show events
eventLoader: _listOfDayEvents,

E é isso, implementamos com sucesso o método para adicionar eventos às datas do calendário e mostrá-lo em nosso aplicativo no calendário. Você pode dar uma olhada em todo o código aqui.

class CustomTableCalendar extends StatefulWidget {
  const CustomTableCalendar({Key? key}) : super(key: key);

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

class _CustomTableCalendarState extends State<CustomTableCalendar> {
  final todaysDate = DateTime.now();
  var _focusedCalendarDate = DateTime.now();
  final _initialCalendarDate = DateTime(2000);
  final _lastCalendarDate = DateTime(2050);
  DateTime? selectedCalendarDate;
  final titleController = TextEditingController();
  final descpController = TextEditingController();

  late Map<DateTime, List<MyEvents>> mySelectedEvents;

  @override
  void initState() {
    selectedCalendarDate = _focusedCalendarDate;
    mySelectedEvents = {};
    super.initState();
  }

  @override
  void dispose() {
    titleController.dispose();
    descpController.dispose();
    super.dispose();
  }

  List<MyEvents> _listOfDayEvents(DateTime dateTime) {
    return mySelectedEvents[dateTime] ?? [];
  }

  _showAddEventDialog() async {
    await showDialog(
        context: context,
        builder: (context) => AlertDialog(
              title: const Text('New Event'),
              content: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                mainAxisSize: MainAxisSize.min,
                children: [
                  buildTextField(
                      controller: titleController, hint: 'Enter Title'),
                  const SizedBox(
                    height: 20.0,
                  ),
                  buildTextField(
                      controller: descpController, hint: 'Enter Description'),
                ],
              ),
              actions: [
                TextButton(
                  onPressed: () => Navigator.pop(context),
                  child: const Text('Cancel'),
                ),
                TextButton(
                  onPressed: () {
                    if (titleController.text.isEmpty &&
                        descpController.text.isEmpty) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        const SnackBar(
                          content: Text('Please enter title & description'),
                          duration: Duration(seconds: 3),
                        ),
                      );
                      //Navigator.pop(context);
                      return;
                    } else {
                      setState(() {
                        if (mySelectedEvents[selectedCalendarDate] != null) {
                          mySelectedEvents[selectedCalendarDate]?.add(MyEvents(
                              eventTitle: titleController.text,
                              eventDescp: descpController.text));
                        } else {
                          mySelectedEvents[selectedCalendarDate!] = [
                            MyEvents(
                                eventTitle: titleController.text,
                                eventDescp: descpController.text)
                          ];
                        }
                      });

                      titleController.clear();
                      descpController.clear();

                      Navigator.pop(context);
                      return;
                    }
                  },
                  child: const Text('Add'),
                ),
              ],
            ));
  }

  Widget buildTextField(
      {String? hint, required TextEditingController controller}) {
    return TextField(
      controller: controller,
      textCapitalization: TextCapitalization.words,
      decoration: InputDecoration(
        labelText: hint ?? '',
        focusedBorder: OutlineInputBorder(
          borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
          borderRadius: BorderRadius.circular(
            10.0,
          ),
        ),
        enabledBorder: OutlineInputBorder(
          borderSide: const BorderSide(color: AppColors.eggPlant, width: 1.5),
          borderRadius: BorderRadius.circular(
            10.0,
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Custom Calendar'),
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => _showAddEventDialog(),
        label: const Text('Add Event'),
      ),
      body: SingleChildScrollView(
        child: Column(
          children: [
            Card(
              margin: const EdgeInsets.all(8.0),
              elevation: 5.0,
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(
                  Radius.circular(10),
                ),
                side: BorderSide(color: AppColors.blackCoffee, width: 2.0),
              ),
              child: TableCalendar(
                focusedDay: _focusedCalendarDate,
                // today's date
                firstDay: _initialCalendarDate,
                // earliest possible date
                lastDay: _lastCalendarDate,
                // latest allowed date
                calendarFormat: CalendarFormat.month,
                // default view when displayed
                // default is Saturday & Sunday but can be set to any day.
                // instead of day number can be mentioned as well.
                weekendDays: const [DateTime.sunday, 6],
                // default is Sunday but can be changed according to locale
                startingDayOfWeek: StartingDayOfWeek.monday,
                // height between the day row and 1st date row, default is 16.0
                daysOfWeekHeight: 40.0,
                // height between the date rows, default is 52.0
                rowHeight: 60.0,
                // this property needs to be added if we want to show events
                eventLoader: _listOfDayEvents,
                // Calendar Header Styling
                headerStyle: const HeaderStyle(
                  titleTextStyle:
                      TextStyle(color: AppColors.babyPowder, fontSize: 20.0),
                  decoration: BoxDecoration(
                      color: AppColors.eggPlant,
                      borderRadius: BorderRadius.only(
                          topLeft: Radius.circular(10),
                          topRight: Radius.circular(10))),
                  formatButtonTextStyle:
                      TextStyle(color: AppColors.ultraRed, fontSize: 16.0),
                  formatButtonDecoration: BoxDecoration(
                    color: AppColors.babyPowder,
                    borderRadius: BorderRadius.all(
                      Radius.circular(5.0),
                    ),
                  ),
                  leftChevronIcon: Icon(
                    Icons.chevron_left,
                    color: AppColors.babyPowder,
                    size: 28,
                  ),
                  rightChevronIcon: Icon(
                    Icons.chevron_right,
                    color: AppColors.babyPowder,
                    size: 28,
                  ),
                ),
                // Calendar Days Styling
                daysOfWeekStyle: const DaysOfWeekStyle(
                  // Weekend days color (Sat,Sun)
                  weekendStyle: TextStyle(color: AppColors.ultraRed),
                ),
                // Calendar Dates styling
                calendarStyle: const CalendarStyle(
                  // Weekend dates color (Sat & Sun Column)
                  weekendTextStyle: TextStyle(color: AppColors.ultraRed),
                  // highlighted color for today
                  todayDecoration: BoxDecoration(
                    color: AppColors.eggPlant,
                    shape: BoxShape.circle,
                  ),
                  // highlighted color for selected day
                  selectedDecoration: BoxDecoration(
                    color: AppColors.blackCoffee,
                    shape: BoxShape.circle,
                  ),
                  markerDecoration: BoxDecoration(
                      color: AppColors.ultraRed, shape: BoxShape.circle),
                ),
                selectedDayPredicate: (currentSelectedDate) {
                  // as per the documentation 'selectedDayPredicate' needs to determine
                  // current selected day
                  return (isSameDay(
                      selectedCalendarDate!, currentSelectedDate));
                },
                onDaySelected: (selectedDay, focusedDay) {
                  // as per the documentation
                  if (!isSameDay(selectedCalendarDate, selectedDay)) {
                    setState(() {
                      selectedCalendarDate = selectedDay;
                      _focusedCalendarDate = focusedDay;
                    });
                  }
                },
              ),
            ),
            ..._listOfDayEvents(selectedCalendarDate!).map(
              (myEvents) => ListTile(
                leading: const Icon(
                  Icons.done,
                  color: AppColors.eggPlant,
                ),
                title: Padding(
                  padding: const EdgeInsets.only(bottom: 8.0),
                  child: Text('Event Title:   ${myEvents.eventTitle}'),
                ),
                subtitle: Text('Description:   ${myEvents.eventDescp}'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
class MyEvents {
  final String eventTitle;
  final String eventDescp;

  MyEvents({required this.eventTitle, required this.eventDescp});

  @override
  String toString() => eventTitle;
}

Como mencionei anteriormente neste artigo, existem algumas excelentes bibliotecas de calendário disponíveis, como flutter_calendar_carousel e syncfusion_flutter_calendar.

A implementação fundamental para todos permanece a mesma. Mesmo os atributos e a customização são muito comparáveis ao que mencionei sobre o TableCalendar neste artigo. Embora os nomes das propriedades sejam diferentes, a funcionalidade permanece a mesma.

Procurei incluir o máximo de detalhes possível para ajudar quem deseja integrar um calendário em seu aplicativo, mas como costumo dizer, a descoberta requer experimentação, e esse sempre foi meu lema. Portanto, brinque com o código e, se precisar de mais informações, sempre poderá consultar a documentação oficial disponível no site pub.dev.

Muito obrigado!