Flutter 개발일지 day 7
문제 인식
일단 가장 큰 문제가, 우리가 각 EveryDay widget마다 ontap메소드로 현재 cursor에 그 EveryDay widget의 날짜를 대입해준다고 해도, container 입장에서는 이걸 알아차릴 방법이 없다는 것이다. 우리가 원하는 것은, 커서가 12월 1일을 가르키고 있다가 12월 2일을 클릭하면 1일의 원은 사라지고 2일에 원이 새로 뜨는 것이었다. 따라서 기존 widget은 원을 지우고, 클릭한 widget은 원을 띄우는 것을 원했다.
하지만 문제는 새로운 widget을 클릭해서 cursor의 값을 update 해줘도 이전 widget에는 아무런 변화가 없기 때문에, 새로 build할 수가 없다는 점이었다… 이 상태로 앱을 실행하면, 우리가 클릭할 때마다 새로 원이 추가된다. Hot Reload하면 당연히 통째로 rebuild되니까 마지막으로 선택한 일자만 빼고 다 지워지긴 한다. 이게 바로 widget이 build되는 시점을 고려하지 못하고 코딩했을 때 생긴 문제점이었다.
Provider 도입
그래서 특정 변수가 바뀌었을때, 그 변수를 참조하고 있는 widget을 자동으로 build 시키는 provider을 도입했다! 사실 getx를 써보고 싶었지만 그래도 일단은 배워봤었던 provider를 써보기로 했다. Provider는 state management를 위한 라이브러리로, 아주 편리하게 변화를 전달할 수 있다. provider를 쓰려면 pubspec.yaml에서 dependency에 추가해줘야 한다.
1. main.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// main.dart의 widget build 부분
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => Cursor(selected: DateTime.now()),
child: MaterialApp(
debugShowCheckedModeBanner: false,
title: 'My_Calender',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MonthCal(),
),
);
}
}
위에서 create를 통해서 Cursor class를 최초로 초기화하고, 앞으로 저 클래스에 대한 변화를 위주로 감지하도록 했다. 그 아래는 child로 MaterialApp을 그대로 넣어주면 된다.
2. customClass.dart
위 이미지는 customClass.dart가 어떻게 변했는지 보여준다.
-
line:6은 먼저 Cursor를 언제 어디서든 참조할 수 있게 됨에 따라 Cursor를 인자로 받을 필요가 없어졌기에 element에서 삭제한 것이다. 아래 생성자에서도 마찬가지.
-
line:20의 HitTestBehavior.translucent는 투명한 부분을 tap해도 반응할 수 있게 한다. EveryDay widget은 text를 담고있는 Positioned를 포함해서 여러 widget이 담겨있는 형태이다. 이때 해당 날짜 칸의 어디를 tap해도 다 반응할 수 있게 만들기 위해서 사용했다.
여기서 문제가 발생하는데, 앞서 이전 달과 다음 달의 일자를 담고 있는 Container에 대해서 날짜를 안보이게 했었다. 하지만 안보이더라도 translucent 옵션으로 인해 그 container를 클릭하면 ontap이 작동하는 문제가 있었다. 따라서 여기에도 삼항연산자를 통해 그 날짜 값이 현재 커서의 달과 일치해야만 동작할 수 있게 했다. -
line:22에서 tap을 하면 그 EveryDay widget으로 cursor의 일자를 바꾸어야하므로 호출해서 값을 대입했다. Provider로 Cursor를 선언했으므로, 그 어디에서든
Provider.of<Cursor>(context)
로 Cursor 인스턴스를 사용할 수 있다. 외부에서 Cursor class의 내부 element 값을 임의로 바꾸는 것은 문제가 발생할 수 있으므로, 내부에서 값을 바꿔주는 함수(changeCursor)를 정의해서 사용했다. 자세한건 후술하겠다. -
line:27, line 34는 현재 커서가 가리키는 날과 이 wiget이 가르키는 날이 같은지 비교하는 삼항연산자다.
** line 34는 추후 수정했는데, selected와 oneDay 뒤에 둘다 .month를 넣어주어야 원활하게 동작한다(안넣으면 시간까지 비교해서 .now()로 초기화했을 때 제대로 인식 못함).
3. Cursor.dart
위 이미지는 Cursor.dart가 어떻게 변했는지 보여준다.
Cursor class는 이 클래스의 값이 바뀔 때마다 listener한테 notify 해줘야한다. 따라서 changeCursor을 호출할 때마다 notify하도록 했다. 즉, Provider로 선언된 Cursor을 가져다가 쓰는 모든 widget들은 notifyListener()가 호출될 때마다 모두 다시 build되게 된다. 앞서 등장한 EveryDay widget의 경우에 다른 곳을 tap 했을 때, 다른 모든 EveryDay widget들은 cursor가 자기 자신을 가르키는지 확인해야하므로 notify 해줘야 한다.
1
2
3
4
5
//customContainer.dart line:22
Provider.of<Cursor>(context, listen: false).changeCursor(widget.oneDay);
//Cursor.dart line:31
notifyListeners();
ontap을 하면 Cursor class의 changeCursor을 호출해서 현재 tap한 EveryDay widget의 일자를 대입하고, 다른 widget들에게 notify해주는 것까지 하나의 세트이다.
4. monthCal.dart
1
EveryDay(Provider.of<Cursor>(context).daylist()[i]),
모든 EveryDay widget들은 이런 형태로 불러올 수 있다. 어짜피 유사 전역변수 형태로 Cursor가 선언되어 있기 때문에, 따로 list를 정의하고 daylist의 리턴을 할당할 필요 없이 바로바로 리스트를 리턴해서 쓸 수 있다.
디자인 변경
글자들이 너무 두껍고 투박해보여서, 예에에전에 정의했던 MyText 위젯에서 fontweight를 조절해서 text를 좀더 가늘게 만들었다. 안타깝게도 한글은 fontweight이 적용되지 않는 문제가 있어서 숫자만 얇아졌다… 한글은 나중에 폰트 적용해서 바꿀 예정이다.
Appbar 기능 추가
MonthCal class의 Appbar에 여러 기능을 추가했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
AppBar(
backgroundColor: const Color(0xFFFAE3D9),
elevation: 0,
title: MyText(
"${Provider.of<Cursor>(context).selected.year}년 ${Provider.of<Cursor>(context).selected.month}월", 20
),
actions: [
IconButton(
onPressed: () {
Provider.of<Cursor>(context, listen: false).plusMonth(false);
}, icon: const Icon(Icons.arrow_back_ios_new_rounded, color: Colors.black54,),
splashRadius: 20,
),
IconButton(
onPressed: () {
Provider.of<Cursor>(context, listen: false).plusMonth(true);
}, icon: const Icon(Icons.arrow_forward_ios_rounded, color: Colors.black54)
),
IconButton(onPressed: () {}, icon: Icon(Icons.change_circle_rounded), color: Colors.black54,),
IconButton(
icon: const Icon(Icons.today, color: Colors.black54,),
onPressed: () {
Provider.of<Cursor>(context, listen: false).changeCursor(DateTime.now());
},
),
]
),
- title : 커서가 가르키는 년, 월을 표시해준다.
- actions : appbar 왼쪽에서부터 하나씩 추가해준다.
- conButton 1 : 이전 달로 가기 버튼
- IconButton 2 : 다음 달로 가기 버튼
- IconButton 3 : 주간, 월간 달력 전환을 위해 만든 버튼이다. 아직 미구현.
- IconButton 4 : cursor를 오늘로 이동시켜주는 버튼
다 기본적인 문법이라 따로 설명이 필요없다. 추가된 Cursor.plusmonth 부분만 설명한다.
1
2
3
4
5
6
7
8
9
10
void plusMonth(bool plus) {
if (plus) {
selected = DateTime(selected.year, selected.month + 1, 1);
notifyListeners();
}
else {
selected = DateTime(selected.year, selected.month - 1, 1);
notifyListeners();
}
}
true를 넣으면 다음달 1일을 가르키고, false를 넣으면 이전달 1일을 가르키도록 한다. 당연히 cursor의 값에 변화가 생겼으므로, 모든 listener들에게 notify 해준다.
**위에서, changeCursor와 plusMonth를 호출하는 Provider.of
배운점
변화가 없는 widget을 어떻게 build 시킬지 진짜 고민이 많았는데, 당연히 필요한 기능이기에 여러 state management를 위한 툴이 잘 구비되어 있었다. 확실히 우리의 경우처럼 특정 변수를 참조하는 widget이 한두개가 아닌 상황은 정말 흔한 일이기 때문에 분명 해답이 있을 줄은 알았는데 그게 provider인지는 몰랐다… 아무튼 직접 도입해서 써보니 진짜 어마어마하게 편한 기능이었다. 마치 전역 변수처럼 언제 어디서나 불러올 수 있다는 것도 큰 장점이고, 해당 인스턴스가 변하면 notifyListener을 통해 알아서 그 변수를 참조하는 모든 widget들을 다시 build 해주는 것도 편리했다. provider를 쓰는 방법을 톡톡히 잘 배웠다.