[프로젝트] 일정 스케줄러
들어가기 앞서...
1. 주요로 봐야하는 점들...
- SQLite 를 이용한 테이터 관리
- 간단한 SQL 문법
- Drift 패키지(SQLite ORM)
- Table Calendar 패키지 (캘린더)
- Getlt 패키지 (Dependency Injection)
- Spinkit 패키지 (이쁜 로딩 위젯)
- Intl 패키지(다국어화)
- TextField (글자 인풋받기)
본문으로...
1. 세팅!
// 실제 출시 시에만
dependencies:
table_calendar: ^3.0.3
intl: ^0.17.0
drift: ^1.4.0
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.0.0
path: ^1.8.1
// 개발 시에만(출시하면 제외된다.)
dev_dependencies:
drift_dev: ^1.4.0
build_runner: ^2.1.7
// 플러그인들이 의존하는 버전이 서로 다르다는 뜻, 몇가지 버전중 밑에 해당하는 것에 의존하겠다. 라는 뜻!
dependency_overrides:
path: ^1.8.1
2. TableCalender 사용법!
// 모두 DateTime 타입이다.
return TableCalendar(
focusedDay: DateTime.now(), // 몇월을 보여줄지
firstDay: DateTime(1800), // 날짜 언제부터 선택 가능
lastDay: DateTime(3000) // 날짜 어디까지 선택 가능
)
이렇게만 넣어줘도 출력된다.
3. TableCalender 세부 세팅
...
class _CalenderState extends State<Calender> {
// 어떤 날짜를 눌렀을지를 담는 변수 선언
DateTime? selectedDay;
@override
Widget build(BuildContext context) {
return TableCalendar(
focusedDay: DateTime.now(),
firstDay: DateTime(1800),
lastDay: DateTime(3000),
// 헤더 스타일
headerStyle: HeaderStyle(
// 2주씩 보이게 하는 버튼
formatButtonVisible: false,
// 제목 가운데로
titleCentered: true,
// 제목 텍스트 스타일
titleTextStyle: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 16.0,
),
),
// 캘린더에서 어떤 특정 날짜를 선택할때 실행된다!
// 선택한 날짜가 selectedDay 파라미터로 들어오게 된다.
// (DateTime? selectedDay;)그래서 상단에 State클래스 안에 저장을 하려고 변수를 선언해둔것이고,
onDaySelected: (DateTime selectedDay, DateTime focusedDay) {
print('selectedDay : $selectedDay');
// 바뀔때마다 setState 로 상단에 선언한 변수에 넣어준다.
setState(() {
this.selectedDay = selectedDay;
});
},
// 특정 날짜가 선택된 지정날짜로 지정 되어야하는지 함수를 넣어주고 bool 값으로 return
true: 그 날짜가 선택 o
false: 그 날짜 선택 x
selectedDayPredicate: (DateTime date) {
if(selectedDay == null) { //
return false; // 어떤 날짜도 되지 않았다.
}
// 비교하고 있는 년월일, 우리가 선택한 년월일이 같다면 선택한것이다.
return date.year == selectedDay!.year &&
date.month == selectedDay!.month &&
date.day == selectedDay!.day;
},
);
}
}
연한 파랑 : 오늘 날짜
진한 파랑 : 내가 선택한 날짜
4. TableCalender 스타일링!
class _CalenderState extends State<Calender> {
DateTime? selectedDay;
@override
Widget build(BuildContext context) {
final defaultBoxDecoration = BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8.0),
);
final defaultTextStyle = TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.w700,
);
return TableCalendar(
focusedDay: DateTime.now(),
firstDay: DateTime(1800),
lastDay: DateTime(3000),
// 헤더 스타일
headerStyle: HeaderStyle(
// 2주씩 보이게 하는 버튼
formatButtonVisible: false,
// 제목 가운데로
titleCentered: true,
// 제목 텍스트 스타일
titleTextStyle: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 16.0,
),
),
calendarStyle: CalendarStyle(
// 오늘 날짜 보여주기 여부
isTodayHighlighted: false,
// 평일 꾸미기
defaultDecoration: defaultBoxDecoration,
// 주말 꾸미기
weekendDecoration: defaultBoxDecoration,
// 선택한 날 꾸미기
selectedDecoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.0),
border: Border.all(
color: PRIMARY_COLOR,
width: 1.0,
)),
// 평일 텍스트 스타일
defaultTextStyle: defaultTextStyle,
// 주말 택스트 스타일
weekendTextStyle: defaultTextStyle,
// 선택 날 텍스트 스타일
selectedTextStyle: defaultTextStyle.copyWith(color: PRIMARY_COLOR)),
// 특정날짜를 선택할때 실행!
...
// 특정 날짜가 선택된 지정날짜로 지정 되어야하는지 함수를 넣어주고 bool 값으로 return
...
}
}
스타일 자세히 보기)
헤더 스타일 HeaderStyle
- 2주 버튼 보이게 하는 여부 (false : 안보이게)
- 제목 가운데로 하는 여부 (true : 가운데로 위치)
- 제목 텍스트 스타일 (굵기 : w700, 크기: 16)
캘린더 스타일 CalenderStyle
- 꾸미기 Decoration
- 오늘 날짜 보여주기 여부 (false: 안보이게)
- 평일 (색상 : 회색[200], 테두리 : 8 만큼)
- 주말 (색상 : 회색[200], 테두리 : 8 만큼)
- 선택 날 (색상 : 흰색, 테두리 : 8 만큼, 테두리 색: 메인색상, 테두리 넓이 : 1)
- 텍스트 스타일 textStyle
- 평일 (색상 : 회색[600], 글씨 무게(굵기) : w700)
- 주말 (색상 : 회색[600], 글씨 무게(굵기) : w700)
- 선택 날 (색상 : 메인 색상, 글씨 무게(굵기) : w700)
5. 이슈 수정하기!
상황 보기 영상)
2022 년 12월 기준 11월 30일 클릭하면
1) 포커싱될 때마다 빨간표시
에러 보기)
#2 BoxDecoration.debugAssertIsValid (package:flutter/src/painting/box_decoration.dart:129:12)
에러를 클릭해서 따라가보니, 동그라미인경우 border radius 를 사용 불가하다. 라고 출력
// box_decoration.dart
@override
bool debugAssertIsValid() {
assert(shape != BoxShape.circle || borderRadius == null); // Can't have a border radius if you're a circle.
return super.debugAssertIsValid();
}
Table Calender 는 날짜를 선택할때 기본값 동그라미로 되어있다.
애니메이션이 진행되는 도중에 동그라미로 변하는데, 동그라미를 사각형으로 변경해주면 된다.
outsideDecoration: BoxDecoration(shape: BoxShape.rectangle),
2) 해당 월로 이동 안됌
이전 날짜를 클릭하면 포커싱된 날을 해당 날짜로 포커싱해주면된다.
class _CalenderState extends State<Calender> {
...
// 추가!
// 선택된 날을 담기 위한 변수 선언(default 는 오늘!)
DateTime focusedDay = DateTime.now();
@override
Widget build(BuildContext context) {
...
return TableCalendar(
// 변경! : DateTiem.now() -> focusedDay
// 이유! : 포커싱하는날을 오늘날만 하는게 아니라 선택하는 날짜에 따라 달라져야한다.
focusedDay: focusedDay,
...
// 특정 날짜를 선택할때 실행!
onDaySelected: (DateTime selectedDay, DateTime focusedDay) {
setState(() {
this.selectedDay = selectedDay;
// 추가!
// 이유! : 선택할때마다 포커싱날짜 변수에 담아주기 위함
this.focusedDay = selectedDay;
});
},
...
);
}
}
6. 다국어 기능 추가!
1) locale 추가
class _CalenderState extends State<Calender> {
...
@override
Widget build(BuildContext context) {
...
return TableCalendar(
locale: 'ko_KR',
...
)
}
2) main.dart 에 패키지 불러오기
2-1) runApp 하면 app 이 실행이 되는데, app 하기전에 intl 패키지를 초기화할 것이다.
결과 intl 패키지에 있는 모든 언어를 사용할 수 있다.
import 'package:intl/date_symbol_data_local.dart';
void main() async {
// fluuter 프레임 워크가 준비가 된 상태인지 확인하는 함수
// runApp() 이 실행되기전 initializeDateFormatting() 함수를 사용하기 위해서 그 전에 프레임 워크가 준비가 되었는지 확인해줘야한다.
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting();
runApp(
MaterialApp(
theme: ThemeData(fontFamily: 'NotoSans'),
home: HomeScreen(),
),
);
}
원래 runApp() 이 실행되면, WidgetsFlutterBinding.ensureInitialized(); 이 자동적으로 실행이 되는데, runApp() 하기전에 실행해야할 코드가 있기때문에 작성해둔것!
WidgetsFlutterBinding.ensureInitialized() 란?
7. TodayBanner 설계
home_screen.dart 에서 selecteddDay 와 focusedDay 를 관리하고 파라미터로 내려줄것이다.
이유: 캘린더 클래스에서 사용하던 selectedDay와 focuesedDay 를 조금더 위클래스에서 상태관리해서 캘린더클래스와 TodayBanner 클래스에 내려주기 위함
그리고 Calender.dart 는 상태관리를 안하니 stless 로 다시 변경해주었다. 부모 클래스에다가 상태관리를 옮겨서 내려줬으니!
class _HomeScreenState extends State<HomeScreen> {
// 선택 날짜 변수(default 는 DateTime(년, 월, 일))
DateTime selectedDay = DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
);
// 포커싱된 날짜 변수, (default 는 현재 날짜)
DateTime focusedDay = DateTime.now();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
Calender(
selectedDay: selectedDay,
focusedDay: focusedDay,
onDaySelected: onDaySelected,
),
SizedBox(height: 8.0),
TodayBanner(
selectedDay: selectedDay,
scheduleCount: 3,
),
SizedBox(height: 8.0),
],
),
),
);
}
// 날짜 선택하는 함수도 위에 관리, 원래 캘린더클래스에 있던 것!
onDaySelected(DateTime selectedDay, DateTime focusedDay) {
print('selectedDay : $selectedDay');
setState(() {
this.selectedDay = selectedDay;
this.focusedDay = selectedDay;
});
}
}
today_banner.dart UI 및 구조 설계
class TodayBanner extends StatelessWidget {
final DateTime selectedDay;
final int scheduleCount;
const TodayBanner(
{Key? key, required this.selectedDay, required this.scheduleCount})
: super(key: key);
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
fontWeight: FontWeight.w700,
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,
),
Text(
'${scheduleCount}개',
style: textStyle,
),
]),
),
);
}
}
8. ScheduleCard 설계
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(
border: Border.all(
width: 1.0,
color: PRIMARY_COLOR,
),
borderRadius: BorderRadius.circular(8.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
_Time(
startTime: startTime,
endTime: endTime,
),
SizedBox(width: 16.0),
_Content(
content: content,
),
SizedBox(width: 16.0),
_Category(
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 _Category extends StatelessWidget {
final Color color;
const _Category({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,
);
}
}
여기서 스타일로 한번 봐야하는 부분은
스케쥴 카드의 content 스타일이 위로 정렬 하려고 한다.
만약, Row 위젯에 crosAxisAlignment: CrossAxisAlignment.start, 추가한다면?
현재 Row() 위젯을 잘 봐야 한다.
시간, 내용, 카테고리 3가지 클래스가 잇다.
Row() 는 가로 형태로 나열하는것이고, crossAxisAlignment: CrossAxisAlignment.start 는 반대축(세로 중 맨 위 부터 시작)
을 한 것이다.
return Container(
decoration: BoxDecoration(
border: Border.all(
width: 1.0,
color: PRIMARY_COLOR,
),
borderRadius: BorderRadius.circular(8.0),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
// 여기다 하면 될까? crosAxisAlignment: CrossAxisAlignment.start,
children: [
_Time(
startTime: startTime,
endTime: endTime,
),
SizedBox(width: 16.0),
_Content(
content: content,
),
SizedBox(width: 16.0),
_Category(
color: color,
),
],
),
),
);
결과)
_content 와 _category 클래스가 같이 올라간것을 볼 수 있다.
하지만 우리가 원하는 결과물)
_content 만 올라가고, _category 는 그대로!
원인 부터 알자!
높이가 Container 위젯이 Row() 위젯 보다 더 높은 높이를 차지하고 있는데도, text 위젯은 차지할수 있는 최대한의 사이즈를 안차지 하고있는 상황이 원인인것이다.
2가지 방법이 있다.
1번째 방법) Strech를 사용해서 보여주는데, SizedBox(height) 를 정적으로 설정하고 하기!
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: Container(color: Colors.blue, child: Text(content)));
}
}
child: SizedBox(
height: 300,
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
2번쨰 방법) IntrinsicHeight() 사용하기!
IntrinsicHeight() 위젯을 사
용하면, child : Row() 위젯 안에 들어있는 위젯중에서 가장 높이가 높은 위젯이랑 같은 사이즈로 만들어준다.
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
마저 다닥다닥 붙어있으니 답답해보이니깐, Padding 적용
// home_screen.dart
...
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ScheduleCard(
startTime: 8, endTime: 9, content: '기상', color: Colors.red),
)
9. ListView 사용!
ScheduleCard 위젯을 여러개 복사해서 붙여넣으면 밑으로 주루룩 출력이 될건데, 어느 순간 화면을 벗어나게 된다.
그렇다면 스크롤이 가능하게끔 하는 기능은 ListView() 를 사용하면 된다.
child: LisetView.builder(
itemCount: 3,
itemBuilder: (context, index) {
return ScheduleCard(
startTime: 8,
endTime: 9,
content: '기상',
color: Colors.red,
)
}
),
추가로 ListView 가 얼만큼의 사이즈로 출력이 되어야하는지, 명시가 안되어있기 때문에 에러가 난다.
그러면 Expanded() 위젯으로 ListView 가 남는 나머지 공간을 모두 차지하라고 명시해주면 된다.
에러 내용 )
Viewports expand in the scrolling direction to fill their container. In this case, a vertical viewport was given an unlimited amount of vertical space in which to expand. This situation typically happens when a scrollable widget is nested inside another scrollable widget.
/*
결론 : 수직 공간이 임의로 정해져있으니, ListView가 출력 될 수 있게 수직 공간을 정해주면 된다.
뷰포트가 스크롤 방향으로 확장되어 컨테이너를 채웁니다. 이 경우 수직 뷰포트에는 확장할 수 있는 수직 공간이 무제한으로 제공되었습니다. 이 상황은 일반적으로 스크롤 가능한 위젯이 다른 스크롤 가능한 위젯 내부에 중첩되어 있을 때 발생합니다.
*/
ListView 의 장점
메모리에 굉장히 유리하다.
아무리 itemCount 숫자가 100이라고 가정한다면?
item Builder 가 0~ 99 까지의 index 를 한번에 출력하려는 것이 아닌 0-9 까지의 인덱스만 출력하고 마지막인덱스에 도달했을때, itemBuilder 가 다음의 index를 가져온다.그렇다면 굉장히 메모리에 부담이 되지 않을것이다.
ListView.spearated(
itemCount:100,
separatorBuilder: (context, index) {
// itemBuilder 한번 불릴때마다 그 다음에 불린다.
return SizedBox(height: 8.0);
},
itemBuilder: (context, index) {
return Schedulecard(
...
);
}
)
10. Floatting button 생성!
10-1) 플로팅 버튼 생성
return Scaffold(
// 플로팅 버튼
floatingActionButton: rederFloatingActionButton(),
body: SafeArea(
...
)
)
// 플로팅 버튼 함수
FloatingActionButton renderFloatingActionButton() {
return FloatingActionButton(
onPressed: () {
// 바텀 시트 모달
showModalBottomSheet(context: context, builder: (_) {
return Container(color: Colors.white, height: 300,);
});
},
backgroundColor: PRIMARY_COLOR,
child: Icon(Icons.add),
);
}
10-2) 바텀 시트 모달 꾸미기
1) TextField() 를 사용하면 클릭하면 키보드 패드가 나온다. 가려지기때문에, 그거에 맞게 올려줘야한다.
//위 아래 왼 오른 쪽으로 시스템적으로 가려진 부분을 viewInsets 로 접근이 가능하다.
MedizQuery.of(context).viewInsets.top
MedizQuery.of(context).viewInsets.bottom
MedizQuery.of(context).viewInsets.left
MedizQuery.of(context).viewInsets.right
@override
Widget build(BuildContext context) {
// 시스템적으로 가려진 부분
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
return Container(
// 컨테이너의 높이는 휴대폰의 전체길이 / 2 + 시스템적으로 가려진 부분
height: MediaQuery.of(context).size.height / 2 + bottomInset,
color: Colors.white,
child: Padding(
// 패딩으로 감싸주고, 가려진 부분(키보드 사이즈 만큼)을 bottom 에서 밀어올려준다.
padding: EdgeInsets.only(bottom: bottomInset),
child: Column(
children: [
TextField(),
],
),
),
);
}
2) 바텀 시트 모달의 맥스를 풀어줘야 한다.
그런데도 바텀 모달시트의 크기가 전체를 먹지 못하는 상황이다.
그렇다면 플로팅 버튼의 모달 여는 부분에 추가를 해준다. isScrollControlled
이유는 showModalBottomSheet 의 기본 크기가 화면의 반크기만 최대사이즈를 갖기때문이다.
FloatingActionButton(
onPressed: () {
showModalBottomSheet(
...
// 추가!
isScrollControlled: true,
...
)
}
)
그리고 BottomSheet 안의 UI 를 작업 해주겠다.
텍스트 필드가 공통적으로 들어가니 컴포넌트로 따로 분리시켜놓다.
// 분리 시켜 놓은 TextFiled 클래스
class CustomTextField extends StatelessWidget {
final String label;
const CustomTextField({Key? key, required this.label}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
// 이유: Column() 위젯을 기본 CrossAxisAlignment 는 center 로 되어있다. 그러니 start 로 변경
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: TextStyle(
color: PRIMARY_COLOR,
fontWeight: FontWeight.w600
),),
TextField(
cursorColor: Colors.grey,
decoration: InputDecoration(
// 테두리 삭제
border: InputBorder.none,
// 채우기 여부
filled: true,
// 채우기 색상
fillColor: Colors.grey[300]
),
),
],
);
}
}
그리고 색깔들을 Row()로 해서 설계 하면 될것 같지만! 저 색상들이 많아지면, 화면 밖으로 넘어가서 이슈가 생기니!
Wrap () 위젯을 사용해주면된다.
// _ColorPicker 클래스
{
Wrap(
// 양옆 간격
spacing: 8.0,
// 위아래 간격
runSpacing:10.0,
children: [
renderColor(Colors.red),
renderColor(Colors.orange),
renderColor(Colors.yellow),
...
]
);
// 공통 적인 부분은 위젯으로 분리
Widget renderColor(Color color) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
width: 32.0,
height: 32.0,
);
}
}
텍스트의 input type 을 정해줄수 있다.
TextField(
keyboardType: TextInputType.number, // 이름, 핸드폰, url, 등 여러가지 있다.
)
포커싱이 되어있는 TextField 에서 포커싱을 해제할수 있다.
TextField 를 포커싱하고 있는 도중에 다른 부분을 클릭하면, 키패트내려가게 하고 싶다!
GestureDetector(
onTap: (){
FocusScope.of(context).requestFocus(FocusNode());
}
)
그리고 isTime : 시간입력하는 것이냐 아니냐를 bool 으로 받아서 분기처리를 하였다.
maxLines, keyboardType, inputFormatters
...
if(isTime) renderTextField(),
if(!isTime) Expanded(child: renderTextField()),
...
}
class CustomTextField extends StatelessWidget {
final String label;
// true - 시간 / false - 내용
final bool isTime;
const CustomTextField({Key? key, required this.label, required this.isTime})
: super(key: key);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(color: PRIMARY_COLOR, fontWeight: FontWeight.w600),
),
TextField(
cursorColor: Colors.grey,
// 최대 줄 1줄, null 은 줄바꿈이 된다.
maxLines: isTime? 1: null,
// 확장시켜주기 위한 파라미터 true, false
expands: !isTime,
// 키보드 타입 설정
keyboardType: isTime ? TextInputType.number : TextInputType.multiline,
// FilteringTextInputFormatter.digitsOnly 는 오직 그타입으로만 입력가능하게 한다.
inputFormatters: isTime ? [FilteringTextInputFormatter.digitsOnly] :[],
decoration: InputDecoration(
// 테두리 삭제
border: InputBorder.none,
filled: true,
fillColor: Colors.grey[300]),
),
],
);
}
}
추가적으로 ... 말 줄임표 나타내는 방법이 있다.
class _Content extends StatelessWidget {
final String content;
const _Content({super.key, required this.content});
@override
Widget build(BuildContext context) {
return Text(
content,
// 최대 줄 수
// maxLines: 3,
// 말줄임표
// overflow: TextOverflow.ellipsis,
);
}
}
11. 프로젝트 테이블 구조!
신기하게도 테이블을 생성을 할 수가 있다.
// model/schedule.dart
import 'package:drift/drift.dart';
class Schedules extends Table {
// PRIMARY KEY
IntColumn get id => integer()();
// 내용
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 createAt => dateTime()();
}
// model/category_color.dart
import 'package:drift/drift.dart';
class CategoryColors extends Table {
// PRIMARY KEY
IntColumn get id => integer()();
// 색상 코드
TextColumn get hexCode => text()();
}
Column 의 기능이 있다!
Id 값은 자동적으로 증가 되게끔 drift 에서 제공을 해준다.
시간은 사용자가 디폴트로 지정할수도 있다.
// 모델 테이블을 만드려면 꼭 import 해줘야한다.
import 'package:drift/drift.dart';
// Table 을 상속해라!
Class Schedules extends Table {
// 기본키, 기본키는 고유값으로 저장을 해야하기 때문에 우리가 직접 입력해주는것이 아닌 자동적으로 들어가게 하자!
IntColumn get id => integer().autoIncrement()();
// 생성 날짜, 생성 날짜는 입력되는 현재의 날짜를 넣어주면 된다.
DateTimeColumn get createAt => dateTime().clientDefulat(
() => DateTime.now(),
)();
}
12. Code Generation, g.dart 파일 생성!
그리고 part 라는 개념이 있다.
기본적인 import 는 private 함수, 클래스는 불러오지 못한다.
하지만 part 는 private 함수도 불러올 수 있다.
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:scheduler_study/model/category_color.dart';
import 'package:scheduler_study/model/schedule.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:drift/native.dart';
// part 는 import 보다 조금 더 넓은 기능을 한다.
// import 는 private 를 불러올 수 없다.
part 'drift_database.g.dart';
@DriftDatabase(
tables: [
Schedules,
CategoryColors,
],
)
// _$LocalDatabase 는 현재 이 클래스에서 선언해놓지 않았다. 하지만 사용가능한 이유는 part 로 불러왔기 때문이다.
class LocalDatabase extends _$LocalDatabase {
LocalDatabase() : super(_openConnection());
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
// getApplicationDocumentsDirectory : 앱을 설치하게 되면, os 에서 앱별로 각각 사용가능한 부분에 지정을 자동으로 해준다. 그것을 가져오는 함수!
final dbFolder = await getApplicationDocumentsDirectory();
// dbFolder 의 경로에 내가 원하는 이름으로 파일을 저장한다.
final file = File(p.join(dbFolder.path, 'db.sqlite'));
return NativeDatabase(file);
});
}
그리고 g.dart 파일을 생성하기 위해 flutter pub run build_runner build
13. 쿼리 호출!
main.dart 에서 쿼리 생성하는 문 작성
class LocalDatabase extends _$LocalDatabase {
LocalDatabase() : super(_openConnection());
// 스케쥴 추가 insert 문
Future<int> createSchedule(SchedulesCompanion data) =>
into(schedules).insert(data);
// 색상 추가 insert 문
Future<int> createCategoryColor(CategoryColorsCompanion data) =>
into(categoryColors).insert(data);
// 카테고리 컬러 다 불러오는 select 문
Future<List<CategoryColor>> getCategoryColors() =>
select(categoryColors).get();
// 스키마를 무조건 override 를 해야한다.
데이터 베이스 상태의 버전 이다.
@override
int get schemaVersion => 1;
}
14. 벨리데이션 하는 법!
onChanged 로 하면 각각 하나마다 validation 상태값을 가지고 있어야한다.
그러니 모든 벨리데이션을 한번에 관리할 수 있는 TextFormField() 위젯을 사용하면 된다.
1) 벨리데이션을 하기 위한 좋지 않은 방법! (onChanged)
TextField(
onChanged: (String? value) {
if(val == null || val.isEmpty) {
return '아이디 입력해라';
}else{
return null;
}
}
),
TextField(
onChanged: (String? value) {
if(val == null || val.isEmpty) {
return '닉네임을 입력해라';
}else{
return null;
}
}
)
2) 한번에 관리하는 좋은 방법!
2-1) 한번에 textFiled 를 관리하고 싶은 해당하는 클래스의 상위클래스에다가 Form 태그를 감싸주면 감싸주고
// 내가 벨리데이션 상태관리를 하고 싶은 부모에다가 Form() 태그를 감싸줘라!
// key 는 Form 을 관리하는 컨트롤러라고 생각하면 된다.
// Stful
final GlobalKey<FormState> formKey = GlobalKey();
Form(
key: formKey,
children: [
_Time(),
_Content(),
],
),
2-2) 저장하는 버튼에는 validation 이 출력하냐 안출력하냐의 분기처리를 적어주고,
자식클래스에는 validator에 null 또는 빈값일때 출력하고 싶은 분기처리를 적어주면된다.
// 부모 클래스
// 저장 버튼 있는 함수
void onSavePressed() {
// formKey 는 생성했는데 Form 위젯과 결합을 안했을때 (Form 위젯에 key 값 안넣을때)
if (formKey.currentState == null) {
return;
}
// 모든 textField 를 검증을 하고서 다 null 값이 다 리턴되면(모두 에러가 안나면) true 리턴
if (formKey.currentState!.validate()) {
// true: TextFormField(validator)은 null 로 리턴을 했다.
print('에러가 없습니다.');
} else {
// false: TextFormField(validator)은 string 값을 리턴한다.
print('에러 가 있습니다.');
}
}
// 자식클래스
// 아이디
TextFormField(
validator: (String? val) {
if(val == null || val.isEmpty) {
return '아이디 값입력해주세요';
} else {
return null;
}
}
),
// 닉네임
TextFormField(
validator: (String? val) {
if(val == null || val.isEmpty) {
return '닉네임 값입력해주세요';
} else {
return null;
}
}
),
결과)
15. ValidationMode 사용해보기!
저장 버튼을 누를때마다 validation 이 나오는 것보다 입력하는것에 따라 자동적으로 벨리데이션 하게 해보자
Form(
key: formKey,
// 자동적으로 벨리데이션 해주는 것!
autovalidateMode: AutovalidateMode.always,
)
16. Form을 저장하는 법!
검증을 하는데 쓰는 validator 를 사용했고, 검증 완료 되고 저장이 될 때의 함수를 알아보자!
// 언제 호출되냐 TextFormField 를 사용하는 상위인 Form()태그에서 save 함수 를 불렀을때 모두 호출된다.
TextFormField(
onSaved: (String? val) {}
)
외부에서 만들어서 넣어주도록 하자!
이유: 실제로는 마지막으로 저장 버튼이 눌렸을때 값들을 가져오는 용도로 사용할 것 이기때문에, 외부에서 함수를 파라미터로 넣어주는 방식으로 선언하자!
// class ScheduleBottomSheet extends StatefulWidget {
int? startTime;
int? endTime;
String? content;
// class _ScheduleBottomSheetState extends State<ScheduleBottomSheet> {
_Time(
onStartSaved: (String? newValue) {
print('newValue $newValue');
startTime = int.parse(newValue!);
},
onEndSaved: (String? newValue) {
endTime = int.parse(newValue!);
},
),
...
_Content(
onSaved: (String? newValue) {
content = newValue;
}
)
// 저장 버튼 누를때
void onSavePressed() {
...
if (formKey.currentState!.validate()) {
// 저장함수 호출!
formKey.currentState!.save();
} else {
print('에러 가 있습니다.');
}
}
17. Select 쿼리 UI 에 적용하기!
색상을 디비에 넣어둔것을 출력해주도록 하겠다.
dependency injection 의존성 주입을 시켜준다.
get_it: ^7.2.0
main.dart 에다가 database 를 접근 가능하게 변수를 만들어놨는데, 하위클래스에서 어떻게 이 변수에 접근할까?
파라미터를 내려주기에는 코드가 길어지고 더러워질거다.
Getit 클래스를 통해서 어디서든 database 를 접근 가능해진다.
import 'package:get_it/get_it.dart';
void main() async {
...
final database = LocalDatabase();
// getit 클래스로 database 어디든 접근 가능해짐
GetIt.I.registerSingleton<LocalDatabase>(database);
}
dataBase 를 접근해서 출력하고 싶은 곳에 적어주자
import 'package:scheduler_study/database/drift_database.dart';
// GetIt 패키지로 의존성을 주입시킨 것이다. 그래서 데이터 베이스에 선언해놓은 함수들 사용가능하다.
FutureBuilder<List<CategoryColor>>(
future: GetIt.I<LocalDatabase>().getCategoryColors(),
builder: (context, snapshot) {
print(snapshot.data);
return _ColorPicker(
colors: snapshot.hasData
? snapshot.data!.map((e) => Color(
int.parse(
'FF${e.hexCode}',
radix: 16,
),
)).toList()
: [],
);
}
),
이렇게 가공을 한이유는?
로컬데이터에 색상을 공통적인(FF부분 이외의 것을 저장시켰기 때문이다.) 부분 이후의 것을 가공해서 출력하기 위함이다.
// print(snapshot.data);
// [CategoryColor(id: 1, hexCode: F44336), CategoryColor(id: 2, hexCode: FF9800), CategoryColor(id: 3, hexCode: FFEB3B), CategoryColor(id: 4, hexCode: FCAF50), CategoryColor(id: 5, hexCode: 2196F3), CategoryColor(id: 6, hexCode: 3F51B5), CategoryColor(id: 7, hexCode: 9C27B0)]
int.parse('FF'${e.hexCode}, radix: 16);
// radix: 16 -> 16 진수로 변환해주기 위함이다. 이걸 넣기전에는 10진수이다.
결과 )
디비에서 잘 불러왔다.
18. 색상 상태관리!
색상을 상태 관리해서 색상을 선택 후 저장 버튼을 누르면 선택한 색상 고유 값이 파라미터로 넘어가길 원한다.
typedef?
함수는 반환타입 or void 를 지정한 후, 함수명과 파라미터로 되어있는 구조이다.
typedef 도 큰 차이 없다.
즉, typedef 를 사용하면 함수들을 변수처럼 사용가능하게 만들어준다.
그럼 그냥 함수를 호출하면 되는것아닌가? 왜 굳이 typedef 를 사용하는데?
클래스안에서 함수를 넣어줘야할 때가 있다.
void add(int a, int b) {
print('a 더하기 b 는 ${a+b}이다.');
}
void subtract(int a, int b){
print('a 빼기 b 는 ${a-b}이다.');
}
typedef Operation(int a, int b);
// 임의 함수
void calculate(int a, int b, Operation oper) {
oper(a, b);
}
// main 함수
void main() {
calculate(1, 2, add); // a 더하기 b 는 3이다.
calculate(3, 2, subtract); // a 빼기 b 는 1이다.
}
함수에 인자로 함수를 넣고, 그값을 받은 calcuate 는 첫번째와 두번째 인자로 세번째 typedef 를 실행한다.
부모클래스에서 자식클래스로 함수를 넘겨주기 위해 ColorIdSetter typedef 로 선언했다.
int selectedColorId = 1;
return _ColorPicker(
colors: ...,
selectedColorId: ...,
colorIdSetter: (int id){
setState((){
selectedColorId = id;
});
}
);
// typedef 정의
typedef ColorIdSetter = void Fuction(int id);
class _ColorPicker extends StatelessWidget {
final List<CategoryColor> colors;
final int selectedColorId;
final ColorIdSetter colorIdSetter; // int id를 파라미터로 보내는 void 함수
const _ColorPicker(
{Key? key,
required this.colors,
required this.selectedColorId,
required this.colorIdSetter})
: super(key: key);
@override
Widget build(BuildContext context) {
return Wrap(
spacing: 8.0,
runSpacing: 10.0,
children: colors
.map((e) => GestureDetector(
onTap: () {
colorIdSetter(e.id);
},
child: renderColor(e, selectedColorId == e.id)))
.toList(),
);
}
19. Schedule 저장해버리기!
import 한 패키지에서 Column 을 두군데에서 받아서 이런 에러가 나는 것인데, drift 에서는 Value 만 사용할것이니 밑에 처럼 추가!
import 'package:drift/drift.dart' show Value;
lib/components/new_schedule_bottom_sheet.dart:44:24: Error: 'Column' is imported from both 'package:drift/src/dsl/dsl.dart' and 'package:flutter/src/widgets/basic.dart'.
child: Column(
^^^^^^
GetIt 패키지를 통해서 <LocalDatabase> 에 선언한 insert 문을 사용할 수 있고, insert 문에 맞춰 values 를 넣어준것이다.
// 선택한 날짜
final DateTime seletedDate;
...
void onSavePressed() async {
// int key 를 반환한다.
final key = GetIt.I<LocalDatabase>().createSchedule(shcedulesCompanion(
date: Value(widget.selectedDate),
startTime: ...,
endTime: ...,
content: ...,
colorId: ...,
));
// 뒤로가기!
Navigator.of(context).pop();
}
20. Stream 으로 쿼리 결과값 받기!
위에서 GetIt 패키지로 로컬 디비에 저장한것을 데이터 통신으로 불러오겠다.
리스트를 출력할때는 스케줄을 추가할때마다 자동적으로 update 될 수 있게! StreamBuilder를 사용할거다!
StreamBuilder 는 상태가 변할때마다 api 를 다시 호출한다.
import 'package:get_it/get_it.dart';
import 'package:scheduler_study/database/drift_database.dart';
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
// streamBuilder 로 선언!
child: StreamBuilder<List<Schedule>>(
stream: GetIt.I<LocalDatabase>().watchSchedules(),
builder: (context, snapshot) {
print(snapshot.data);
return ListView.separated(
separatorBuilder: (context, index) {
return SizedBox(height: 8.0);
},
itemCount: 3,
itemBuilder: (context, index) {
return ScheduleCard(
startTime: 8,
endTime: 9,
content: '기상',
color: Colors.red);
},
);
})),
select 문을 Stream 으로 작성하는 방법은 .watch() 를 써주면된다. (.get() 이 아닌!)
// drift_database.dart
// update 될때마다 계속 지속적으로 받는 Stream
Stream<List<Schedule>> watchSchedules() =>
select(schedules).watch();
21. TimeZone 시차고려하기!
12월 26일 하나의 스케쥴을 입력한 상태이고 where 을 통해 오늘 날짜로 filter 된 스케쥴 만 나오게 해놓은 상태이다.
초기 앱 처음 킬때
flutter: ---------------origin----------------
flutter: [Schedule(id: 1, content: test1, date: 2022-12-26 00:00:00.000, startTime: 11, endTime: 12, colorId: 2, createAt: 2022-12-26 15:40:59.000)]
flutter: ---------------filterd----------------
flutter: 2022-12-26 00:00:00.000
flutter: [Schedule(id: 1, content: test1, date: 2022-12-26 00:00:00.000, startTime: 11, endTime: 12, colorId: 2, createAt: 2022-12-26 15:40:59.000)]
27일 클릭후 다시 26일로 돌아온 결과
flutter: ---------------origin----------------
flutter: [Schedule(id: 1, content: test1, date: 2022-12-26 00:00:00.000, startTime: 11, endTime: 12, colorId: 2, createAt: 2022-12-26 15:40:59.000)]
flutter: ---------------filterd----------------
flutter: 2022-12-26 00:00:00.000Z
flutter: []
갑자기 date 뒤에 z 가 생기고, filter 한 스케쥴이 빈배열이 되었다!!!
z 는 UTC 시간에서 0 시 를 의미한다.
UTC 는 전세계에서 나라별로 시차를 나타내는 Standard 라고 생각하면된다.
참고로 우리나라는 +9 이다. 중심시간으로 부터 9시간 더해주면된다.
z 안붙어있는 date 는 현재 시간
z 붙어있는 date 는 UTC 기준으로 0시, 중심시간이다.
둘중을 통일 시켜주면된다.
처음 저장할때 UTC 기준으로 저장을 시켜놓던지, 날짜를 선택할때 한국기준으로 시간을 저장하던지!
class _HomeScreenState extends State<HomeScreen> {
// 첫렌더링 할때 UTC 기준으로 생성한게 아닌 현재시간을 한국 기준으로 정한것
DateTiem seletedDay = DateTime(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
)
}
// UTC 기준이다. 그렇기 때문에 캘린더에서 날짜를 옮기는 순간 UTC 기준으로 시간이 변한다.
onDaySelected(DateTime selectedDay, DatreTime focusedDay) {
setState() {
...
}
}
첫랜더링 할때 시간 저장하는 것을 utc 로 바꿔주자! 그럼 첫 렌더링때도 utc기준으로 날짜를 저장하는 것을 볼수 있다.
DateTiem seletedDay = DateTime.utc(
DateTime.now().year,
DateTime.now().month,
DateTime.now().day,
)
결과)
flutter: ---------------origin----------------
flutter: [Schedule(id: 1, content: test1, date: 2022-12-26 09:00:00.000, startTime: 11, endTime: 12, colorId: 2, createAt: 2022-12-26 16:52:51.000)]
22. 쿼리에 직접 Where 필터 적용하기!
stream: GetIt.I<LocalDatabase>().watchSchedules(),
builder: (context, snapshot) {
print('---------------origin----------------');
print(snapshot.data);
List<Schedule> schedules = [];
if (snapshot.hasData) {
schedules = snapshot.data!
.where((element) => element.date.toUtc() == selectedDate)
.toList();
print('---------------filterd----------------');
print(selectedDate);
print(schedules);
}
결과)
flutter: ---------------filterd----------------
flutter: 2022-12-26 00:00:00.000Z
flutter: [Schedule(id: 1, content: test1, date: 2022-12-26 09:00:00.000, startTime: 11, endTime: 12, colorId: 2, createAt: 2022-12-26 16:52:51.000)]
하지만 이런식의 filter 는 snapshot.data 를 전부다 가져온다...
그러면 메모리에 스케쥴들을 전부 다 불러오는 것이기 때문에, 메모리가 꽉차게 된다.
그러니 flutter 단에서 다 불러와서 filter 를 하는 것이 아니라, drit 의 쿼리문에서 제한을 둬야한다!!
// update 될때마다 계속 지속적으로 받는 Stream
// 이게 원래 정석이다.
Stream<List<Schedule>> watchSchedules(DateTime date) {
final query = select(schedules);
query.where((tbl) => tbl.date.equals(date));
return query.watch();
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
// select(schedules).watch() 되는것랑 같다!
return (select(schedules)..where((tbl) => tbl.date.equals(date))).watch();
//예 )
int number = 3;
final response = number.toString();
// '3' -> String
final response2 = number..toString();
// 3 -> int
// .toString() 함수는 실행이 되는데, 함수가 실행이 되는 대상이 리턴이 된다.
처음에는 streambuilder 에서 받아온 값으로 fliter 를 했는데, 쿼리문에서 where 절을 걸어서 해당되는 값만 데이터가 넘어오니, streambuilder 결과로 넘어 온값은 필터문을 삭제해주었다!
child: StreamBuilder<List<Schedule>>(
stream: GetIt.I<LocalDatabase>().watchSchedules(selectedDate),
builder: (context, snapshot) {
if(!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if(snapshot.hasData && snapshot.data!.isEmpty){
// 데이터 통신은 잘해서 data 는 잘 받아오는데, 리스트가 빈 값이라면?
return Center(
child: Text('스케줄이 없습니다.'),
);
}
return ListView.separated(
itemCount: snapshot.data!.length,
separatorBuilder: (context, index) {
return SizedBox(height: 8.0);
},
itemBuilder: (context, index) {
// 각각의 인덴스별 스케쥴
final schedule = snapshot.data![index];
return ScheduleCard(
startTime: schedule.startTime,
endTime: schedule.endTime,
content: schedule.content,
color: Colors.red);
},
);
})),
23. Join 쿼리 사용하기!
현재 색상을 해당 되는 id 값을 넘겨주는데, 카테고리 컬러 테이블과 스케쥴 테이블의 색상 선택 값을 비교하려면 테이블 간의 JOIN을 해줘야한다.
// model/ schedule_with_color.dart 새로 생성
import 'package:scheduler_study/database/drift_database.dart';
// 두개의 테이블의 대한 값을 담기 위함
class ScheduleWithColor {
final Schedule schedule;
final CategoryColor categoryColor;
ScheduleWithColor({
required this.schedule,
required this.categoryColor,
});
}
// update 될때마다 계속 지속적으로 받는 Stream
Stream<List<ScheduleWithColor>> watchSchedules(DateTime date) {
// .. : ..where() 함수가 실행이 되는데, 실행되는 대상이 주체(select(schedules)) 가 된다.
// return (select(schedules)..where((tbl) => tbl.date.equals(date))).watch();
final query = select(schedules).join([
// innerJoin(join 할 테이블, 조건)
// join 할때는 equalsExp 를 사용
// schedules 와 categoryColors 조인하는 데 categoryColors.id 와 schedules.colorId 같은 것만!
innerJoin(categoryColors, categoryColors.id.equalsExp(schedules.colorId))
]);
// 테이블이 두 개이기 때문에 schedules 라고 테이블 정의 해준다.
query.where(schedules.date.equals(date));
// rows : 필터링된 모든 데이터들, row : 각각의 데이터 를 ScheduleWithColor 에 넣어준거다.
// readeTable(테이블) : 해당하는 테이블 읽어와라
return query.watch().map((rows) => rows
.map((row) => ScheduleWithColor(
schedule: row.readTable(schedules),
categoryColor: row.readTable(categoryColors)))
.toList());
}
// 데이터 베이스 상태의 버전이다. 데이터 구조가 변경될 때 올려주면 된다.
@override
int get schemaVersion => 1;
}
// <ScheduleWithColor> 로 타입변경
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) {
// 데이터 통신은 잘해서 data 는 잘 받아오는데, 리스트가 빈 값이라면?
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 ScheduleCard(
startTime: scheduleWithColor.schedule.startTime,
endTime: scheduleWithColor.schedule.endTime,
content: scheduleWithColor.schedule.content,
color: Color(int.parse(
'FF${scheduleWithColor.categoryColor.hexCode}',
radix: 16)));
},
);
})),
24. Order By 로 정렬하기!
프론트에서 정렬을 해줄 수 있지만, 백엔드에서 데이터를 던져줄때 정렬된것을 던져주면 훨씬 편하다.
// update 될때마다 계속 지속적으로 받는 Stream
Stream<List<ScheduleWithColor>> watchSchedules(DateTime date) {
// .. : ..where() 함수가 실행이 되는데, 실행되는 대상이 주체(select(schedules)) 가 된다.
// return (select(schedules)..where((tbl) => tbl.date.equals(date))).watch();
final query = select(schedules).join([
// innerJoin(join 할 테이블, 조건)
// join 할때는 equalsExp 를 사용
// schedules 와 categoryColors 조인하는 데 categoryColors.id 와 schedules.colorId 같은 것만!
innerJoin(categoryColors, categoryColors.id.equalsExp(schedules.colorId))
]);
// 테이블이 두 개이기 때문에 schedules 라고 테이블 정의 해준다.
query.where(schedules.date.equals(date));
// 추가!
// 정렬
query.orderBy([
// asc -> 오름 차순
// desc -> descending 내림차순
OrderingTerm.asc(schedules.startTime),
]);
// rows : 필터링된 모든 데이터들, row : 각각의 데이터 를 ScheduleWithColor 에 넣어준거다.
// readeTable(테이블) : 해당하는 테이블 읽어와라
return query.watch().map((rows) => rows
.map((row) => ScheduleWithColor(
schedule: row.readTable(schedules),
categoryColor: row.readTable(categoryColors)))
.toList());
}
25. TodayBanner 위젯 개선하기!
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,
);
}),
26. Delete 쿼리 적용하기!
오른쪽에서 왼쪽으로 스와이프 했을때 지워지는 애니메이션 기능
삭제 쿼리 작성
// 테이블의 전체 데이터 삭제
removeSchedule() => delete(schedules).go();
// 외부에서 삭제할 id 값을 받아서 schedules 테이블의 id 와 같은지 비교하고 같은 row 를 삭제를 하겠다. int 값을 리턴 받을 수 있다.(삭제한 id 의 int 값이다.)
Future<int> = removeSchedule(int id) =>
(delete(schedules)..where((tbl) => tbl.id.equals(id))).go();
swiper 를 하면 삭제가능하게끔 하는 위젯이있다.
// 스와이퍼 가능하게끔 해주는 위젯
return Dismissible(
// 어떤 위젯이 삭제가 됐는지 인식을 한다. (유니크한 키)
key: ObjectKey(scheduleWithColor.schedule.id),
// 어디로 스와이프 할지 설정
direction: DismissDirection.endToStart,
// 스와이프 됐을때 함수 설정
onDismissed: (DismissDirection direction){
GetIt.I<LocalDatabase>().removeSchedule(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))),
);
27. Update 쿼리 적용하기!
특정 스케쥴을 눌렀을때 바텀시트 열리면서, 작성한 부분 출력되고, 변경후 저장기능까지!
update 쿼리 작성
// update 문
// schedules 을 업데이트 한다. 테이블 id와 선택한 스케쥴의 id 같은 row 컬럼을 찾아서 data 값으로 변경해라!
Future<int> updateScheduleById(int id, SchedulesCompanion data) =>
(update(schedules)..where((tbl) => tbl.id.equals(id))).write(data);
스케쥴 클릭하면 스케쥴 바텀 시트 열리게하고 id 값 보내게 설정
우선은 id 를 받을 수 있게 설정(필수가 아니게 설정하고 : 기존에 사용하던거를 헤치지않게)
class ScheduleBottomSheet extends StatefulWidget {
final DateTime selectedDate;
final int? scheduleId;
const ScheduleBottomSheet(
{Key? key, required this.selectedDate, this.scheduleId})
: super(key: key);
GestureDetector 위젯 추가해서 파라미터 넘겨준다.
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(
selectedDate: 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))),
),
);
