Flutter에서 Calendar를 만드는 도중 Event를 표시해야했는데 pub.dev에 table_calendar가 커스텀하기 편하다는 이야기가 많았습니다.
그리하여, table_calendar를 사용하여 캘린더를 만들어보도록 하겠습니다.
table_calendar의 코드가 계속 업데이트 되다보니 이와 관련된 블로그와 정리된 글들을 따라해도 쉽게 진행되지 않았습니다. 이로 인해, 삽질을 하며 패키지 공식 문서와 Github에 실제 코드를 참고하며 구현하게 되었습니다.
혹시나 같은 문제로 잘 해결되지 않는 분들을 위해 글을 작성하게 되었습니다.
본론으로 들어와 Flutter에서 Calendar에 Event를 표시하기 위한 방법을 다뤄보도록 하겠습니다. 먼저, 코드의 실행 결과는 아래와 같이 나타나게 됩니다.
캘린더 결과는 2022년 8월 달 데이터를 표시한 것 입니다.
패키지를 사용하기 위해 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
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)),
}) ;
Event 일정들은 하늘색 동그라미로, 현재 요일은 기본 색깔로 표시해두었으며, 사용자가 클릭한 날은 파란색 동그라미로 표시해두었습니다.
Event를 지정해주는 부분은 calendarBuilders에서 markerBuilder 부분입니다.
markerBuilder에서 date 값을 출력해보시면 DateTime 형태인 YYYY:MM:DD 00:00:00.000Z (ex, 2022-08-27 00:00:00.000Z) 로 출력될 것 입니다.
(환경에 따라 조금씩 다를 수 있다는 점 고려해주시면 좋을 것 같습니다.)
이때, date가 UTC를 기준으로 하고 있어 맨 마지막에 Z가 나오게 되어 date와 _events[date] 를 비교하게 되면 _events[date] 에서 null이 뜨게 되는 것을 확인하실 수 있습니다.
즉, 기본 DateTime 을 사용하면 Z가 포함되어 있지 않기 때문에 date를 _events에 키 값으로 주게 되면 HashMap에서 해당 키가 존재하지 않아 null을 띄우게 되는 것 입니다.
결론적으로, date는 현재 달에 모든 요일 DateTime 값이 들어오게 되는데 _events 일정과 비교하여 해당되는 요일들만 표시해준 것 입니다.
...
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,
),
);
}
},
),
)
)
);
}
}