[Flutter] table_calendar

Jinil Kim·2022년 8월 24일

Flutter

목록 보기
2/3
post-thumbnail

Flutter에서 Calendar를 만드는 도중 Event를 표시해야했는데 pub.dev에 table_calendar가 커스텀하기 편하다는 이야기가 많았습니다.

그리하여, table_calendar를 사용하여 캘린더를 만들어보도록 하겠습니다.

Official URL: table_calendar

[pub.dev] table_calendar

table_calendar의 코드가 계속 업데이트 되다보니 이와 관련된 블로그와 정리된 글들을 따라해도 쉽게 진행되지 않았습니다. 이로 인해, 삽질을 하며 패키지 공식 문서와 Github에 실제 코드를 참고하며 구현하게 되었습니다.

혹시나 같은 문제로 잘 해결되지 않는 분들을 위해 글을 작성하게 되었습니다.


본론으로 들어와 Flutter에서 Calendar에 Event를 표시하기 위한 방법을 다뤄보도록 하겠습니다. 먼저, 코드의 실행 결과는 아래와 같이 나타나게 됩니다.

결과

캘린더 결과는 2022년 8월 달 데이터를 표시한 것 입니다.


1. pubspec.yaml

패키지를 사용하기 위해 pubspec.yaml 파일에 table_calendar: ^3.0.6 을 추가해줍니다.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  table_calendar: ^3.0.6

2. Event

Event를 등록할 때, 제목 + 내용 등을 일정에 기록하기 위해 많이 사용하곤합니다. 반면에, 출석체크와 같이 표시만 하면 될 때에는 위와 같은 내용을 저장할 필요가 없기에 아래와 같이 Event 클래스를 정의해주었습니다.

class Event {
  final DateTime date ;
  Event({required this.date});
}

이후, 아래와 같이 Event 들을 정의해줍니다.
데이터베이스와 연동되어 있는 경우에는 데이터만 가져와주면 되겠죠?

final _events = LinkedHashMap(
    equals: isSameDay,
  )..addAll({
    DateTime(2022, 8, 4) : Event(date: DateTime(2022, 8, 4)),
    DateTime(2022, 8, 6) : Event(date: DateTime(2022, 8, 6)),
    DateTime(2022, 8, 7) : Event(date: DateTime(2022, 8, 7)),
    DateTime(2022, 8, 9) : Event(date: DateTime(2022, 8, 9)),
    DateTime(2022, 8, 11) : Event(date: DateTime(2022, 8, 11)),
    DateTime(2022, 8, 14) : Event(date: DateTime(2022, 8, 14)),
  }) ;

3. Table Calendar

Event 일정들은 하늘색 동그라미로, 현재 요일은 기본 색깔로 표시해두었으며, 사용자가 클릭한 날은 파란색 동그라미로 표시해두었습니다.

Event를 지정해주는 부분은 calendarBuilders에서 markerBuilder 부분입니다.
markerBuilder에서 date 값을 출력해보시면 DateTime 형태인 YYYY:MM:DD 00:00:00.000Z (ex, 2022-08-27 00:00:00.000Z) 로 출력될 것 입니다.

3.1 DateTime(date.year, date.month, date.day)를 사용한 이유

(환경에 따라 조금씩 다를 수 있다는 점 고려해주시면 좋을 것 같습니다.)

이때, date가 UTC를 기준으로 하고 있어 맨 마지막에 Z가 나오게 되어 date와 _events[date] 를 비교하게 되면 _events[date] 에서 null이 뜨게 되는 것을 확인하실 수 있습니다.
즉, 기본 DateTime 을 사용하면 Z가 포함되어 있지 않기 때문에 date를 _events에 키 값으로 주게 되면 HashMap에서 해당 키가 존재하지 않아 null을 띄우게 되는 것 입니다.

결론적으로, date는 현재 달에 모든 요일 DateTime 값이 들어오게 되는데 _events 일정과 비교하여 해당되는 요일들만 표시해준 것 입니다.

3.2 Table Calendar Code


... 

DateTime _now = DateTime.now();
CalendarFormat _calendarFormat = CalendarFormat.month;
DateTime? _selectedDay;
List<String> days = ['_', '월', '화', '수', '목', '금', '토', '일'];
  
... 

  
	TableCalendar(
    	  // 달에 첫 날 
          firstDay: DateTime(_now.year, _now.month, 1),
          // 달에 마지막 날
          lastDay: DateTime(_now.year, _now.month + 1, 0),
          focusedDay: _now,
          calendarFormat: _calendarFormat,
          daysOfWeekHeight: 30,
          headerVisible: false,
          calendarStyle: const CalendarStyle(
              selectedDecoration: BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              )
          ),
          selectedDayPredicate: (day) {
            return isSameDay(_selectedDay, day);
          },
          // 사용자가 캘린더에 요일을 클릭했을 때
          onDaySelected: (selectedDay, focusedDay) {
            if (!isSameDay(_selectedDay, selectedDay)) {
              // Call `setState()` when updating the selected day
              setState(() {
                _selectedDay = selectedDay;
                _now = focusedDay;
              });
            }
          },
          // 캘린더의 포맷을 변경 (CalendarFormat.month 로 지정)
          onFormatChanged: (format) {
            if (_calendarFormat != format) {
              // Call `setState()` when updating calendar format
              setState(() {
                _calendarFormat = format;
              });
            }
          },
          onPageChanged: (focusedDay) {
            // No need to call `setState()` here
            _now = focusedDay;
          },
          calendarBuilders: CalendarBuilders(
            dowBuilder: (context, day) {
              return Center(child: Text(days[day.weekday])) ;
            },
            // Event Marker  
            markerBuilder: (context, date, events) {
              DateTime _date = DateTime(date.year, date.month, date.day);
              if ( isSameDay(_date, _events[_date] )) {
                return Container(
                    width: MediaQuery.of(context).size.width * 0.11,
                    padding: const EdgeInsets.only(bottom: 5),
                    decoration: const BoxDecoration(
                      color: Colors.lightBlue,
                      shape: BoxShape.circle,
                    ),
                );
              }
            },
          ),
        )

전체 코드

import 'dart:collection';

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

void main() => runApp(const MyApp());

class Event {
  final DateTime date ;
  Event({required this.date});
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Table Calendar'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  DateTime _now = DateTime.now();
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime? _selectedDay;
  List<String> days = ['_', '월', '화', '수', '목', '금', '토', '일'];

  final _events = LinkedHashMap(
    equals: isSameDay,
  )..addAll({
    DateTime(2022, 8, 4) : Event(date: DateTime(2022, 8, 4)),
    DateTime(2022, 8, 6) : Event(date: DateTime(2022, 8, 6)),
    DateTime(2022, 8, 7) : Event(date: DateTime(2022, 8, 7)),
    DateTime(2022, 8, 9) : Event(date: DateTime(2022, 8, 9)),
    DateTime(2022, 8, 11) : Event(date: DateTime(2022, 8, 11)),
    DateTime(2022, 8, 14) : Event(date: DateTime(2022, 8, 14)),
  }) ;


  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        padding: const EdgeInsets.only(top: 30),
        height: MediaQuery.of(context).size.height * 0.45,
        child: TableCalendar(
          firstDay: DateTime(_now.year, _now.month, 1),
          lastDay: DateTime(_now.year, _now.month + 1, 0),
          focusedDay: _now,
          calendarFormat: _calendarFormat,
          daysOfWeekHeight: 30,
          headerVisible: false,
          calendarStyle: const CalendarStyle(
              selectedDecoration: BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              )
          ),
          selectedDayPredicate: (day) {
            // Use `selectedDayPredicate` to determine which day is currently selected.
            // If this returns true, then `day` will be marked as selected.

            // Using `isSameDay` is recommended to disregard
            // the time-part of compared DateTime objects.
            return isSameDay(_selectedDay, day);
          },
          onDaySelected: (selectedDay, focusedDay) {
            if (!isSameDay(_selectedDay, selectedDay)) {
              // Call `setState()` when updating the selected day
              setState(() {
                _selectedDay = selectedDay;
                _now = focusedDay;
              });
            }
          },
          onFormatChanged: (format) {
            if (_calendarFormat != format) {
              // Call `setState()` when updating calendar format
              setState(() {
                _calendarFormat = format;
              });
            }
          },
          onPageChanged: (focusedDay) {
            // No need to call `setState()` here
            _now = focusedDay;
          },
          calendarBuilders: CalendarBuilders(
            dowBuilder: (context, day) {
              return Center(child: Text(days[day.weekday])) ;
            },
            markerBuilder: (context, date, events) {
              DateTime _date = DateTime(date.year, date.month, date.day);
              if ( isSameDay(_date, _events[_date] )) {
                return Container(
                    width: MediaQuery.of(context).size.width * 0.11,
                    padding: const EdgeInsets.only(bottom: 5),
                    decoration: const BoxDecoration(
                      color: Colors.lightBlue,
                      shape: BoxShape.circle,
                    ),
                );
              }
            },
          ),
        )
      )
    );
  }
}

References

  1. https://pub.dev/packages/table_calendar
profile
Slave of coding

0개의 댓글