1)
2) component
/calendar.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
class Calendar extends StatelessWidget {
final DateTime? selectedDay;
final DateTime focusedDay;
final OnDaySelected onDaySelected;
const Calendar({
Key? key,
required this.selectedDay,
required this.focusedDay,
required this.onDaySelected,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final defaultBoxDeco = BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(6.0),
);
final defaultTextStyle = TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.w700,
);
return SafeArea(
child: Column(
children: [
TableCalendar(
locale: 'ko_KR',
focusedDay: focusedDay,
firstDay: DateTime(1800),
lastDay: DateTime(3000),
headerStyle: HeaderStyle(
titleCentered: true,
formatButtonVisible: false,
titleTextStyle: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 16.0,
),
),
calendarStyle: CalendarStyle(
isTodayHighlighted: false,
defaultDecoration: defaultBoxDeco,
weekendDecoration: defaultBoxDeco,
selectedDecoration: defaultBoxDeco.copyWith(
color: Colors.white,
border: Border.all(
color: PRIMARY_COLOR,
width: 1.0,
),
),
outsideDecoration: BoxDecoration(
shape: BoxShape.rectangle,
),
defaultTextStyle: defaultTextStyle,
weekendTextStyle: defaultTextStyle,
selectedTextStyle: defaultTextStyle.copyWith(
color: PRIMARY_COLOR,
),
),
onDaySelected: onDaySelected,
selectedDayPredicate: (DateTime day) {
if (selectedDay == null) {
return false;
}
return day.year == selectedDay!.year &&
day.month == selectedDay!.month &&
day.day == selectedDay!.day;
},
),
],
),
);
}
}
/custom_text_field.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CustomTextField extends StatelessWidget {
final FormFieldSetter<String>? onSaved;
final String label;
final String initialValue;
// true - 시간 : false - 내용
final bool isTime;
const CustomTextField({
Key? key,
required this.label,
required this.isTime,
required this.onSaved,
required this.initialValue,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: PRIMARY_COLOR,
fontWeight: FontWeight.w600,
),
),
if (isTime) renderTextField(),
if (!isTime) Expanded(child: renderTextField()),
],
);
}
Widget renderTextField() {
return TextFormField(
onSaved: onSaved,
// null이 return 되면 에러가 없다.
// 에러가 있으면 에러를 String 값으로 리턴해준다.
validator: (String? val) {
if (val == null || val.isEmpty) {
return '값을 입력해주세요';
}
if (isTime) {
int time = int.parse(val);
if (time < 0) {
return '0 이상의 숫자를 입력해주세요';
}
if (time > 24) {
return '25 이하의 숫자를 입력해주세요';
}
if (val.length > 500) {
return '500자 이하의 글만 입력해주세요';
}
}
return null;
},
maxLines: isTime ? 1 : null,
expands: !isTime,
initialValue: initialValue,
keyboardType: isTime ? TextInputType.number : TextInputType.multiline,
inputFormatters: isTime ? [FilteringTextInputFormatter.digitsOnly] : [],
cursorColor: Colors.grey,
decoration: InputDecoration(
border: InputBorder.none,
filled: true,
fillColor: Colors.grey[300],
suffixText: isTime ? '시' : null,
),
);
}
}
/schedule_bottom_sheet.dart
import 'package:calendar_scheduler/component/custom_text_field.dart';
// drift안에 Column도 있어서 Value만 쓰겠다는 의미
import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import '../const/colors.dart';
import '../database/drift_database.dart';
class ScheduleBottomSheet extends StatefulWidget {
final DateTime selectedDay;
final int? scheduleId;
const ScheduleBottomSheet({
Key? key,
required this.selectedDay,
this.scheduleId,
}) : super(key: key);
@override
State<ScheduleBottomSheet> createState() => _ScheduleBottomSheetState();
}
class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
final GlobalKey<FormState> formKey = GlobalKey();
int? startTime;
int? endTime;
String? content;
int? selectedColorId;
@override
Widget build(BuildContext context) {
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: FutureBuilder<Schedule>(
future: widget.scheduleId == null
? null
: GetIt.I<LocalDatabase>().getScheduleById(widget.scheduleId!),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text('스케줄을 불러올 수 없습니다.'),
);
}
// FutureBuilder 처음 실행됐고
// 로딩중일 때
if (snapshot.connectionState != ConnectionState.none &&
!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
// Future가 실행이 되고
// 값이 있는데 단 한번도 startTime이 세팅되지 않았을 때
if (snapshot.hasData && startTime == null) {
startTime = snapshot.data!.startTime;
endTime = snapshot.data!.endTime;
content = snapshot.data!.content;
selectedColorId = snapshot.data!.colorId;
}
return SafeArea(
child: Container(
height: MediaQuery.of(context).size.height / 2 + bottomInset,
color: Colors.white,
child: Padding(
padding: EdgeInsets.only(bottom: bottomInset),
child: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 8.0,
top: 16.0,
),
child: Form(
key: formKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_Time(
onStartSaved: (String? val) {
startTime = int.parse(val!);
},
onEndSaved: (String? val) {
endTime = int.parse(val!);
},
startInitialValue: startTime?.toString() ?? '',
endInitialValue: endTime?.toString() ?? '',
),
SizedBox(height: 8.0),
_Content(
onSaved: (String? val) {
content = val;
},
initialValue: content ?? '',
),
SizedBox(height: 16.0),
FutureBuilder<List<CategoryColor>>(
future:
GetIt.I<LocalDatabase>().getCategoryColors(),
builder: (context, snapshot) {
if (snapshot.hasData &&
selectedColorId == null &&
snapshot.data!.isNotEmpty) {
selectedColorId = snapshot.data![0].id;
}
return _ColorPicker(
colors:
snapshot.hasData ? snapshot.data! : [],
selectedColorId: selectedColorId,
colorIdSetter: (int id) {
setState(() {
selectedColorId = id;
});
},
);
}),
SizedBox(height: 8.0),
_SaveButton(
onPressed: onSavePressed,
),
],
),
),
),
),
),
);
}),
);
}
Future<void> onSavePressed() async {
// formKey는 생성을 했는데
// Form 위젯과 결합을 안했을 때
if (formKey.currentState == null) {
return;
}
if (formKey.currentState!.validate()) {
formKey.currentState!.save();
if (widget.scheduleId == null) {
await GetIt.I<LocalDatabase>().createSchedule(
SchedulesCompanion(
date: Value(widget.selectedDay),
startTime: Value(startTime!),
endTime: Value(endTime!),
content: Value(content!),
colorId: Value(selectedColorId!),
),
);
} else {
await GetIt.I<LocalDatabase>().updateScheduleById(
widget.scheduleId!,
SchedulesCompanion(
date: Value(widget.selectedDay),
startTime: Value(startTime!),
endTime: Value(endTime!),
content: Value(content!),
colorId: Value(selectedColorId!),
),
);
}
Navigator.of(context).pop();
} else {
print('에러가 있습니다.');
}
}
}
class _Time extends StatelessWidget {
final FormFieldSetter<String>? onStartSaved;
final FormFieldSetter<String>? onEndSaved;
final String startInitialValue;
final String endInitialValue;
const _Time({
Key? key,
required this.onStartSaved,
required this.onEndSaved,
required this.startInitialValue,
required this.endInitialValue,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: CustomTextField(
label: '시작 시간',
isTime: true,
onSaved: onStartSaved,
initialValue: startInitialValue,
),
),
SizedBox(width: 16.0),
Expanded(
child: CustomTextField(
label: '마감 시간',
isTime: true,
onSaved: onEndSaved,
initialValue: endInitialValue,
),
),
],
);
}
}
class _Content extends StatelessWidget {
final FormFieldSetter<String>? onSaved;
final String initialValue;
const _Content({
Key? key,
required this.onSaved,
required this.initialValue,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: CustomTextField(
label: '내용',
isTime: false,
onSaved: onSaved,
initialValue: initialValue,
),
);
}
}
typedef ColorIdSetter = void Function(int id);
class _ColorPicker extends StatelessWidget {
final List<CategoryColor> colors;
final int? selectedColorId;
final ColorIdSetter colorIdSetter;
const _ColorPicker({
Key? key,
required this.colors,
required this.selectedColorId,
required this.colorIdSetter,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 10,
children: colors
.map(
(e) => GestureDetector(
onTap: () {
colorIdSetter(e.id);
},
child: renderColor(
e,
selectedColorId == e.id,
),
),
)
.toList(),
);
}
Widget renderColor(CategoryColor color, bool isSelected) {
return Container(
width: 32.0,
height: 32.0,
decoration: BoxDecoration(
color: Color(
int.parse(
'FF${color.hexCode}',
radix: 16,
),
),
shape: BoxShape.circle,
border: isSelected
? Border.all(
width: 4.0,
color: Colors.black,
)
: null,
),
);
}
}
class _SaveButton extends StatelessWidget {
final VoidCallback onPressed;
const _SaveButton({
Key? key,
required this.onPressed,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: onPressed,
style: ElevatedButton.styleFrom(
primary: PRIMARY_COLOR,
),
child: Text(
'저장',
),
),
),
],
);
}
}
/schedule_card.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:flutter/material.dart';
class ScheduleCard extends StatelessWidget {
final int startTime;
final int endTime;
final String content;
final Color color;
const ScheduleCard({
Key? key,
required this.startTime,
required this.endTime,
required this.content,
required this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
border: Border.all(
width: 1.0,
color: PRIMARY_COLOR,
),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_Time(startTime: startTime, endTime: endTime),
SizedBox(width: 16.0),
_Content(content: content),
SizedBox(width: 16.0),
_Color(color: color),
],
),
),
),
);
}
}
class _Time extends StatelessWidget {
final int startTime;
final int endTime;
const _Time({
Key? key,
required this.startTime,
required this.endTime,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontWeight: FontWeight.w600,
color: PRIMARY_COLOR,
fontSize: 16.0,
);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"${startTime.toString().padLeft(2, '0')}:00",
style: textStyle,
),
Text(
"${endTime.toString().padLeft(2, '0')}:00",
style: textStyle.copyWith(
fontSize: 10.0,
),
),
],
);
}
}
class _Content extends StatelessWidget {
final String content;
const _Content({
Key? key,
required this.content,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Text(content),
);
}
}
class _Color extends StatelessWidget {
final Color color;
const _Color({
Key? key,
required this.color,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
width: 16.0,
height: 16.0,
);
}
}
/today_banner.dart
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:calendar_scheduler/model/schedule_with_color.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
class TodayBanner extends StatelessWidget {
final DateTime selectedDay;
const TodayBanner({
Key? key,
required this.selectedDay,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontWeight: FontWeight.w600,
color: Colors.white,
);
return Container(
color: PRIMARY_COLOR,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${selectedDay.year}년 ${selectedDay.month}월 ${selectedDay.day}일',
style: textStyle,
),
StreamBuilder<List<ScheduleWithColor>>(
stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDay),
builder: (context, snapshot) {
int count = 0;
if(snapshot.hasData){
count = snapshot.data!.length;
}
return Text(
'$count개',
style: textStyle,
);
}
),
],
),
),
);
}
}
3) const
/colors.dart
import 'package:flutter/material.dart';
const PRIMARY_COLOR = Color(0xFF0DB2B2);
4) database
/drift_database.dart
// private 값들은 불러올 수 없다.
import 'dart:io';
import 'package:calendar_scheduler/model/category_color.dart';
import 'package:calendar_scheduler/model/schedule.dart';
import 'package:calendar_scheduler/model/schedule_with_color.dart';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
// private 값까지 불러올 수 있다.
part 'drift_database.g.dart';
@DriftDatabase(
tables: [
Schedules,
CategoryColors,
],
)
class LocalDatabase extends _$LocalDatabase {
LocalDatabase() : super(_openConnection());
Future<int> createSchedule(SchedulesCompanion data) =>
into(schedules).insert(data);
Future<int> createCategoryColor(CategoryColorsCompanion data) =>
into(categoryColors).insert(data);
Future<Schedule> getScheduleById(int id) =>
(select(schedules)..where((tbl) => tbl.id.equals(id))).getSingle();
Future<List<CategoryColor>> getCategoryColors() =>
select(categoryColors).get();
Future<int> updateScheduleById(int id, SchedulesCompanion data) =>
(update(schedules)..where((tbl) => tbl.id.equals(id))).write(data);
Future<int> removeSchedule(int id) =>
(delete(schedules)..where((tbl) => tbl.id.equals(id))).go();
Stream<List<ScheduleWithColor>> watchSchedules(DateTime date) {
final query = select(schedules).join([
innerJoin(categoryColors, categoryColors.id.equalsExp(schedules.colorId)),
]);
query.where(schedules.date.equals(date));
query.orderBy([
// asc -> ascending 오름차순
// desc -> descending 내림차순
OrderingTerm.asc(schedules.startTime),
]);
return query.watch().map(
(rows) => rows
.map(
(row) => ScheduleWithColor(
schedule: row.readTable(schedules),
categoryColor: row.readTable(categoryColors),
),
)
.toList(),
);
}
@override
int get schemaVersion => 1;
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
});
}
/drift_database.g.dart
5) model
/category_color.dart
import 'package:drift/drift.dart';
class CategoryColors extends Table {
// PRIMARY KEY
IntColumn get id => integer().autoIncrement()();
// 색상 코드
TextColumn get hexCode => text()();
}
/schedule.dart
import 'package:drift/drift.dart';
class Schedules extends Table {
// PRIMARY KEY
IntColumn get id => integer().autoIncrement()();
// 내용
TextColumn get content => text()();
// 일정 날짜
DateTimeColumn get date => dateTime()();
// 시작 시간
IntColumn get startTime => integer()();
// 끝 시간
IntColumn get endTime => integer()();
// Category Color Table ID
IntColumn get colorId => integer()();
// 생성 날짜
DateTimeColumn get createdAt => dateTime().clientDefault(
() => DateTime.now(),
)();
}
/schedule_with_color.dart
import 'package:calendar_scheduler/database/drift_database.dart';
class ScheduleWithColor {
final Schedule schedule;
final CategoryColor categoryColor;
ScheduleWithColor({
required this.schedule,
required this.categoryColor,
});
}
6) screen
/home_screen.dart
import 'package:calendar_scheduler/component/calendar.dart';
import 'package:calendar_scheduler/component/schedule_bottom_sheet.dart';
import 'package:calendar_scheduler/component/schedule_card.dart';
import 'package:calendar_scheduler/component/today_banner.dart';
import 'package:calendar_scheduler/const/colors.dart';
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:calendar_scheduler/model/schedule_with_color.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:table_calendar/table_calendar.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
DateTime selectedDay = DateTime.utc(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
DateTime focusedDay = DateTime.now();
@override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: renderFloatingButton(),
body: Column(
children: [
Calendar(
selectedDay: selectedDay,
focusedDay: focusedDay,
onDaySelected: onDaySelected,
),
SizedBox(height: 8.0),
TodayBanner(
selectedDay: selectedDay,
),
SizedBox(height: 8.0),
_ScheduleList(
selectedDate: selectedDay,
),
],
),
);
}
onDaySelected(DateTime selectedDay, DateTime focusedDay) {
setState(() {
this.selectedDay = selectedDay;
this.focusedDay = selectedDay;
});
}
FloatingActionButton renderFloatingButton() {
return FloatingActionButton(
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (_) {
return ScheduleBottomSheet(
selectedDay: selectedDay,
);
},
);
},
child: Icon(
Icons.add,
),
);
}
}
class _ScheduleList extends StatelessWidget {
final DateTime selectedDate;
final int? scheduleId;
const _ScheduleList({
Key? key,
required this.selectedDate,
this.scheduleId,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: StreamBuilder<List<ScheduleWithColor>>(
stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
if (snapshot.hasData && snapshot.data!.isEmpty) {
return Center(child: Text('스케줄이 없습니다.'));
}
return ListView.separated(
itemCount: snapshot.data!.length,
separatorBuilder: (context, index) {
return SizedBox(height: 8.0);
},
itemBuilder: (context, index) {
final ScheduleWithColor = snapshot.data![index];
return Dismissible(
key: ObjectKey(ScheduleWithColor.schedule.id),
direction: DismissDirection.endToStart,
onDismissed: (DismissDirection direction) {
GetIt.I<LocalDatabase>()
.removeSchedule(ScheduleWithColor.schedule.id);
},
child: GestureDetector(
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (_) {
return ScheduleBottomSheet(
selectedDay: selectedDate,
scheduleId: ScheduleWithColor.schedule.id,
);
},
);
},
child: ScheduleCard(
startTime: ScheduleWithColor.schedule.startTime,
endTime: ScheduleWithColor.schedule.endTime,
content: ScheduleWithColor.schedule.content,
color: Color(
int.parse(
'FF${ScheduleWithColor.categoryColor.hexCode}',
radix: 16),
),
),
),
);
},
);
}),
),
);
}
}
7) main.dart
import 'package:calendar_scheduler/database/drift_database.dart';
import 'package:calendar_scheduler/screen/home_screen.dart';
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart';
import 'package:intl/date_symbol_data_local.dart';
const DEFAULT_COLORS = [
// 빨강
'F44336',
// 주황
'FF9800',
// 노랑
'FFEB3B',
// 초록
'FCAF50',
// 파랑
'2196F3',
// 남
'3F51B5',
// 보라
'9C27B0',
];
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting();
final database = LocalDatabase();
GetIt.I.registerSingleton(database);
final colors = await database.getCategoryColors();
if (colors.isEmpty) {
for (String hexCode in DEFAULT_COLORS) {
database.createCategoryColor(
CategoryColorsCompanion(
hexCode: Value(hexCode),
),
);
}
}
print(await database.getCategoryColors());
runApp(
MaterialApp(
theme: ThemeData(
fontFamily: 'NotoSans',
),
home: HomeScreen(),
),
);
}
8)
'개발이 좋아서 > Flutter가 좋아서' 카테고리의 다른 글
[flutter] Scrollable Widgets_ListView (0) | 2023.01.26 |
---|---|
[flutter] Scrollable Widgets_SingleChildScrollView (0) | 2023.01.26 |
[flutter] 켈린더 스케쥴러_database 생성 (0) | 2023.01.18 |
[flutter] 켈린더 스케쥴러_UI 구현 (0) | 2023.01.17 |
[flutter] 켈린더 스케쥴러_환경세팅 (0) | 2023.01.12 |