# 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) 에 정리해뒀다.
    • 아래는 동작 비디오. 유튜브에서 볼 수 있다.

미리보기 (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
  • 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
  • 기본적인 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
  • 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
  • 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
  • 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
  • onLongPress에서는 delete를 담당한다. showDialog로 삭제의사를 확인한다.
  • trailingonChanged를 통해서 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
  • TaskTile에서 필요로하는 callback함수들을 지정해준다.
  • Provider를 매번 부를수도있고 Consumer를 사용해서 Refactor 해줄수도 있다.