# Todo with Flutter state management
# 0. Final Product
Provider
패턴의 state management를 이용하여 todo list를 만들었다.bloc
패턴은 스트리머를 만들기 때문에 event stream과 event type과 같은 많은 클래스를 만들어야한다.provider
는 상태관리를ChangeNotifier
에게 넘기기 때문에 더 간결하다- 엄밀히 말하면
bloc
은 비즈니스 로직과 ui를 구분해주기 위함이고provider
는 dependency injection을 위함이다. - react를 사용할때도 state management를 뭘로 해야 하는가에 대한 개발자들의 끝도없는 고민을 볼 수 있었는데 flutter에서도 그러함을 볼 수 있다. 이럴때 대체로 답은 상황에 따라서 혹은 둘 다 같이 써야한다 임을 알 수 있다.
- 초심자 입장에서는 flutter가 권장하는 provider를 사용하는 것이 편하다.
- 자세한 것은 App Architecture | KIHYEON KWON (opens new window) 에 정리해뒀다.
- 아래는 동작 비디오. 유튜브에서 볼 수 있다.
# 1. Main
- Firebase는 flutter와 마찬가지로 빠르게 업데이트가 되고 있고 그에 따라 configuration도 빠르게 변하고 있어 그때 그때 공식문서를 참고하는 것이 답이다.
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'screens/task_screen.dart';
import 'models/task_data.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (BuildContext context) => TaskData(),
child: MaterialApp(
home: TaskScreen(),
),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ChangeNotiferProvider
를 이용해서 전역에 Provide를 해준다.builder
프로퍼티는 create`로 대체되었다.
# 2. Models
models/task.dart
import 'package:provider/provider.dart';
class Task {
final String content;
bool isDone;
Task({required this.content, this.isDone = false});
void checkDone() {
isDone = !isDone;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
- 기본적인 Task 모델.
models/task_data.dart
import 'package:flutter/foundation.dart';
import 'package:state_management_flutter/models/task.dart';
class TaskData extends ChangeNotifier {
List<Task> _tasks = [
Task(content: "Finish Flutter"),
Task(content: "Workout"),
Task(content: "Shower")
];
List<Task> get tasks {
return _tasks;
}
int get taskCount {
return _tasks.length;
}
void addTask(String newTask) {
_tasks.add(Task(content: newTask));
notifyListeners();
}
void checkTask(Task task) {
task.checkDone();
notifyListeners();
}
void deleteTask(Task task) {
_tasks.remove(task);
notifyListeners();
}
}
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
28
29
30
31
32
33
34
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
28
29
30
31
32
33
34
- Task를 관리하는 TaskData 모델.
- Flutter에서는 getter 메소드를 만들때
get
을 붙여준다. tasks
를 프라이빗으로 관리한후get
메소드로 리턴을 해주면된다.- 여기에 더 관리를 하려면 immutable list view 형식으로 리턴해주면 더 확실히 보호할 수 있다.
- 내부 데이터가 변경될때는
notifyListeners
를 통해 변경을 알린다. - checkTask와 deleteTask에 현재는
Task
를 넣고 있지만index
번호를 넣어서_tasks
에서 찾아서 제거하는 방식이 더 효율적이다.
# 2. Screens
screens/task_screen.dart
import 'package:flutter/material.dart';
import 'package:state_management_flutter/widgets/tasks_list.dart';
import 'package:state_management_flutter/screens/add_task_screen.dart';
import 'package:state_management_flutter/models/task.dart';
import 'package:provider/provider.dart';
import 'package:state_management_flutter/models/task_data.dart';
class TaskScreen extends StatelessWidget {
Widget build(BuildContext context) {
List<Task> tasks = Provider.of<TaskData>(context).tasks;
return Scaffold(
backgroundColor: Colors.lightBlueAccent,
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: EdgeInsets.only(top: 60, left: 20, right: 20, bottom: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
child: Icon(
Icons.list,
size: 30,
color: Colors.lightBlueAccent,
),
backgroundColor: Colors.white,
radius: 30),
SizedBox(height: 30),
Text('Todo',
style: TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.white,
)),
Text('${tasks.length} tasks to do',
style: TextStyle(
fontSize: 16,
color: Colors.white,
))
],
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20)),
),
child: TasksList(),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: (() {
showModalBottomSheet(
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
builder: (context) => SingleChildScrollView(
child: Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom),
child: AddTaskScreen())));
}),
child: Icon(Icons.add),
),
);
}
}
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
List<Task> tasks = Provider.of<TaskData>(context).tasks;
를 통해 Provider에서 tasks 데이터를 갖고온다.showModalBottomSheet
을 통해서AddTaskScreen
을 열어준다.
screens/add_task_screen.dart
import 'package:flutter/material.dart';
import 'package:state_management_flutter/models/task.dart';
import 'package:provider/provider.dart';
import 'package:state_management_flutter/models/task_data.dart';
class AddTaskScreen extends StatelessWidget {
Widget build(BuildContext context) {
String newTaskTitle = '';
return Container(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("Add Task",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.lightBlueAccent,
fontSize: 20,
)),
TextField(
textAlign: TextAlign.center,
decoration: const InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.lightBlueAccent,
)),
hintText: 'Write here'),
onChanged: (newText) {
newTaskTitle = newText;
}),
ElevatedButton(
onPressed: () {
Provider.of<TaskData>(context, listen: false)
.addTask(newTaskTitle);
Navigator.pop(context);
},
child: Text("Add"),
style: ElevatedButton.styleFrom(primary: Colors.lightBlueAccent))
],
),
);
}
}
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
onPressed
에서 Provieder의 메소드인addTask
를 접근한다.Navigator.pop(context)
로 모달을 꺼준다.
# 3. Widgets
task_tile.dart
import 'package:flutter/material.dart';
class TaskTile extends StatelessWidget {
final String content;
final bool isDone;
final Function(bool?) checkCallback;
final Function() longPressCallback;
TaskTile(
{required this.content,
required this.isDone,
required this.checkCallback,
required this.longPressCallback});
Widget build(BuildContext context) {
return ListTile(
onLongPress: () {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text("Delete Task"),
content: Text("Do you want to delete this task?"),
actions: [
FlatButton(
child: Text("No"),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text("Yes"),
onPressed: () {
longPressCallback();
Navigator.of(context).pop();
})
],
);
},
);
},
title: Text(content,
style: TextStyle(
decoration: isDone ? TextDecoration.lineThrough : null)),
trailing: Checkbox(
value: isDone,
onChanged: checkCallback,
),
);
}
}
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
onLongPress
에서는 delete를 담당한다.showDialog
로 삭제의사를 확인한다.trailing
의onChanged
를 통해서checkCallback
을 불러온다.
tasks_list.dart
import 'package:flutter/material.dart';
import 'package:state_management_flutter/widgets/task_tile.dart';
import 'package:state_management_flutter/models/task.dart';
import 'package:provider/provider.dart';
import 'package:state_management_flutter/models/task_data.dart';
class TasksList extends StatelessWidget {
Widget build(BuildContext context) {
List<Task> tasks = Provider.of<TaskData>(context).tasks;
return ListView.builder(
itemBuilder: (context, index) {
return TaskTile(
content: tasks[index].content,
isDone: tasks[index].isDone,
checkCallback: (bool? newValue) {
Provider.of<TaskData>(context, listen: false)
.checkTask(tasks[index]);
},
longPressCallback: () {
Provider.of<TaskData>(context, listen: false)
.deleteTask(tasks[index]);
});
},
itemCount: tasks.length,
);
}
}
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
28
29
30
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
28
29
30
- TaskTile에서 필요로하는
callback
함수들을 지정해준다. - Provider를 매번 부를수도있고 Consumer를 사용해서 Refactor 해줄수도 있다.