Todo List built with Flutter

Author Avatar
Klein 1月 05, 2019

Step by step tutorial how to build Todo List with Flutter.

flutter是google开发的移动端UI框架,支持android和ios。该框架使用dart语言进行开发,在skia的基础上开发了一套公共组件达到android与ios共用代码的目的。

第一步

打开你的终端,运行以下命令:

1
flutter create todo_list

打开 lib/main.dart 文件,引入 Flutter 提供的 material 库:

1
import 'package:flutter/material.dart';

main 方法是整个 flutter 应用的入口,一般只调用 runApp,当然,我们也可以在这个函数里做其他事情,比如:让你的应用全屏显示:

1
2
3
4
5
6
7
import 'package:flutter/services.dart';
// void main() => runApp(MyApp());

void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
runApp(MyApp());
}

在 flutter 中所有的组件叫做 widgetwidget 既可以是 stateless 也可以是 stateful。最上层的组件应该是无状态的,所以在这里我们创建一个 statelessWidget

1
class MyApp extends StatelessWidget {}

每一个 widget 都应该覆盖 build 方法。该方法返回你的布局 widget(containerpadding, Flex 等等)或包含业务逻辑的 statefulWidget以构建UI界面。

本文里我们使用 Material 组件:

1
2
3
4
5
6
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp();
}
}

添加标题:

1
2
3
4
5
6
7
8
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
);
}
}

我们用 Scaffold 为我们的应用程序构建一个主页。

Scaffold 是 Material 库中提供的页面脚手架,它提供了一些基础的布局(导航栏,appbar等)。

1
2
3
4
5
6
7
8
9
10
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
home: Scaffold(
),
);
}
}

添加一个应用程序的头部来显示标题:

1
2
3
4
5
6
return MaterialApp(
title: 'Todo List',
home: Scaffold(
appBar: AppBar(title: Text('Todo List')),
),
);

body 部分是由 todoList 组成的,所以我们添加这一行,并在后面实现这个类:

1
2
3
4
5
6
7
return MaterialApp(
title: 'Todo List',
home: Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: TodoList(),
),
);

渲染列表

状态组件的基本结构下所示:

1
2
3
4
5
6
7
8
9
10
11
12
// todo_list.dart
class TodoList extends StatefulWidget {
@override
_TodoListState createState() => _TodoListState();
}

class _TodoListState extends State<TodoList> {
@override
Widget build(BuildContext context) {
return Container();
}
}

引入 TodoList widget:

1
2
// todo_list.dart
import 'package:todo_list/todo_list.dart';

将每一项 todo 待办事项抽象为一个类:

1
2
3
4
5
6
7
// todo.dart
class Todo {
Todo({this.title, this.isDone = false});

String title;
bool isDone;
}

并且在 todo_list.dart 中引用:

1
2
// todo_list.dart
import 'package:todo_list/todo.dart';

创建一个仅由 Todo 类组成的 list:

1
2
3
// todo_list.dart
class _TodoListState extends State<TodoList> {
List<Todo> todos = [];

使用 ListView 渲染待办事项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// todo_list.dart
class _TodoListState extends State<TodoList> {
List<Todo> todos = [];

_buildItem() {}

@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: _buildItem,
itemCount: todos.length,
);
}
}

实现 _buildItem 方法,每一次渲染 todo 的时候都必须调用它。

使用 material 库的 CheckboxListTile

1
2
3
4
5
6
7
// todo_list.dart
// _buildItem(){}
Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];
return CheckboxListTile(
);
}

Value 表示待办事项是否已完成

1
2
3
4
5
6
// todo_list.dart
final todo = todos[index];
return CheckboxListTile(
value: todo.isDone,
);
}

Title 表示该 CheckboxListTile 的主要内容

1
2
3
4
5
// todo_list.dart
return CheckboxListTile(
value: todo.isDone,
title: Text(todo.title),
);

最后,我们需要处理每个列表项目的点击

1
2
3
4
5
6
7
8
// todo_list.dart
return CheckboxListTile(
value: todo.isDone,
title: Text(todo.title),
onChanged: (bool isChecked) {
_toggleTodo(todo, isChecked);
},
);

_toggleTodo 的实现非常简单:

1
2
3
4
5
6
// todo_list.dart
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
});
}

todo列表项目的渲染和更新已经完成,现在开始实现添加 todo 列表项目的 UI 及功能:

添加一个 FloatingActionButton

1
2
3
4
5
6
7
8
9
10
11
// main.dart
home: Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: TodoList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// 😢
},
),
),

我们需要访问 TodoList 的状态,但是直接从父级无状态 widget 操作子级状态似乎不太好。

1
2
3
4
5
6
7
8
9
10
11
12
// main.dart
/* home: Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: TodoList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// 😢
},
),
),*/
home: TodoList(),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// todo_list.dart
_addTodo() {}

@override
Widget build(BuildContext context) {
// return ListView.builder(
// itemBuilder: _buildItem,
// itemCount: todos.length,
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
body: ListView.builder(
itemBuilder: _buildItem,
itemCount: todos.length,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addTodo,
),
);

当我们点击 FloatingActionButton 时,弹出一个对话框

1
2
3
4
5
6
7
8
9
10
11
12
// todo_list.dart
// _addTodo() {}
_addTodo() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('New todo'),
);
},
);
}

在对话框中添加一个文本输入框和两个按钮:CancelAdd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// todo_list.dart
// _addTodo() {}
_addTodo() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('New todo'),
content: TextField(),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
),
FlatButton(
child: Text('Add'),
),
],
);
},
);
}

对话框不仅仅是叠加层,而且实际是一个路由,因此要处理 Cancel 操作,我们可以在当前上下文的 Navigator 调用 pop 方法。

1
2
3
4
5
6
7
// todo_list.dart
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),

为了获取用户在文本输入框中输入的值以创建 todo 项目,我们需要创建一个 TextEditingController

1
2
3
4
5
// todo_list.dart
class _TodoListState extends State<TodoList> {
List<Todo> todos = [];

TextEditingController controller = new TextEditingController();

并且添加到 TextField

1
2
3
4
// todo_list.dart
return AlertDialog(
title: Text('New todo'),
content: TextField(),

最后,创建新 todo 并将其添加到现有 todo 列表中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// todo_list.dart
FlatButton(
child: Text('Add'),
onPressed: () {
print(controller.value.text);
controller.clear();
setState(() {
final todo = new Todo(title: controller.value.text);
todos.add(todo);
controller.clear();
Navigator.of(context).pop();
});
},
),

在做些细小的 UX 改进:通过对 TextFieldautofocus 属性设置为 true使其自动对焦文本输入框并使键盘自动弹出

1
2
3
4
5
6
7
8
// todo_list.dart
return AlertDialog(
title: Text('New todo'),
// content: TextField(controller: controller),
content: TextField(
controller: controller,
autofocus: true,
),

重构

到这里,整个项目已经正常工作,但是 todo_list.dart 非常混乱并且可读性不好。可以看到,最复杂的方法是 _addTodo ,所以我们开始重写这个方法,似乎可以将 AlertDialog 分离成一个独立的 widget,但我们现在不可以这么做,因为该 widget 依赖 父级 widget 的 setState

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
// todo_list.dart
_addTodo() {
showDialog<Todo>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('New todo'),
content: TextField(
controller: controller,
autofocus: true,
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Add'),
onPressed: () {
// setState(() {
final todo = new Todo(title: controller.value.text);
// todos.add(todo);
controller.clear();
Navigator.of(context).pop(todo);
// });
},
),
],
);

为了能够在 _addTodo 方法中能接收 Todo 类,我们需要使它成为异步并等待 showDialog 函数结果(如果它被解除则为null,否则为Todo的实例)

1
2
3
4
5
6
7
8
// todo_list.dart
// _addTodo() {
// showDialog<Todo>(
_addTodo() async {
final todo = await showDialog<Todo>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
1
2
3
4
5
6
7
8
9
10
11
// todo_list.dart
);
},
);

if (todo != null) {
setState(() {
todos.add(todo);
});
}
}

现在我们对父级 widget 没有任何依赖关系,因此我们可以将 AlertDialog 分离为一个独立的 widget

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
// new_todo_dialog.dart
import 'package:flutter/material.dart';

import 'package:todo_list/todo.dart';

class NewTodoDialog extends StatelessWidget {
final controller = new TextEditingController();

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('New todo'),
content: TextField(
controller: controller,
autofocus: true,
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text('Add'),
onPressed: () {
final todo = new Todo(title: controller.value.text);
controller.clear();

Navigator.of(context).pop(todo);
},
),
],
);
}
}

在 TodoList 中引用:

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
// todo_list.dart
import 'package:todo_list/new_todo_dialog.dart';

// TextEditingController controller = new TextEditingController();
// return AlertDialog(
// title: Text('New todo'),
// content: TextField(
// controller: controller,
// autofocus: true,
// ),
// actions: <Widget>[
// FlatButton(
// child: Text('Cancel'),
// onPressed: () {
// Navigator.of(context).pop();
// },
// ),
// FlatButton(
// child: Text('Add'),
// onPressed: () {
// final todo = new Todo(title: controller.value.text);
// controller.clear();
//
// Navigator.of(context).pop(todo);
// },
// ),
// ],
// );
return NewTodoDialog();

下一步——提炼 todo list 组件

列表部分也可以视为 stateless widget,状态相关的逻辑可以由父级处理:

首先将 TodoList 重命名为 TodoListScreen

1
2
3
4
5
6
7
8
9
10
11
12
13
// todo_list.dart
import 'package:todo_list/new_todo_dialog.dart';

// class TodoList extends StatefulWidget {
class TodoListScreen extends StatefulWidget {
@override
// _TodoListState createState() => _TodoListState();
_TodoListScreenState createState() => _TodoListScreenState();
}

// class _TodoListState extends State<TodoList> {
class _TodoListScreenState extends State<TodoListScreen> {
List<Todo> todos = [];

重命名文件:lib/todo_list.dart => lib/todo_list——screen.dart

并且修改引用关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

// import 'package:todo_list/todo_list.dart';
import 'package:todo_list/todo_list_screen.dart';

void main() {
SystemChrome.setEnabledSystemUIOverlays([]);
Widget build(BuildContext context) {
return MaterialApp(
title: 'Todo List',
// home: TodoList(),
home: TodoListScreen(),
);
}
}

然后将列表相关的逻辑分离成一个独立的 stateless widget

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
// todo_list.dart
import 'package:flutter/material.dart';

class TodoList extends StatelessWidget {
_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
});
}

Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];

return CheckboxListTile(
value: todo.isDone,
title: Text(todo.title),
onChanged: (bool isChecked) {
_toggleTodo(todo, isChecked);
},
);
}

@override
Widget build(BuildContext context) {
return ListView.builder(
itemBuilder: _buildItem,
itemCount: todos.length,
);
}
}

并且从 TodoListScreen 移除相关逻辑

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
// todo_list_screen.dart
import 'package:todo_list/todo_list.dart';

// _toggleTodo(Todo todo, bool isChecked) {
// setState(() {
// todo.isDone = isChecked;
// });
// }
//
// Widget _buildItem(BuildContext context, int index) {
// final todo = todos[index];
//
// return CheckboxListTile(
// value: todo.isDone,
// title: Text(todo.title),
// onChanged: (bool isChecked) {
// _toggleTodo(todo, isChecked);
// },
// );
// }
//
_addTodo() async {
final todo = await showDialog<Todo>(
context: context,
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Todo List')),
// body: ListView.builder(
// itemBuilder: _buildItem,
// itemCount: todos.length,
// ),
body: TodoList(),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: _addTodo,

现在让我们 review 一下我们的 TodoList widget

别忘了加载 todo 类

1
2
// todo_list.dart
import 'package:todo_list/todo.dart';

它也没有 todos ,所以我们让父部件传递给它

1
2
3
4
5
// todo_list.dart
class TodoList extends StatelessWidget {
TodoList({@required this.todos});

final List<Todo> todos;
1
2
3
4
5
6
// todo_list_screen.dart
appBar: AppBar(title: Text('Todo List')),
// body: TodoList(),
body: TodoList(
todos: todos,
),

_toggleTodo 方法依赖 setState,所以我们将它移回父级 widget

1
2
3
4
5
6
7
// todo_list.dart
// _toggleTodo(Todo todo, bool isChecked) {
// setState(() {
// todo.isDone = isChecked;
// });
// }
//
1
2
3
4
5
6
7
8
// todo_list_screen.dart
List<Todo> todos = [];

_toggleTodo(Todo todo, bool isChecked) {
setState(() {
todo.isDone = isChecked;
});
}

并且将其作为属性传给 TodoList

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// todo_list.dart
import 'package:todo_list/todo.dart';

typedef ToggleTodoCallback = void Function(Todo, bool);

class TodoList extends StatelessWidget {
// TodoList({@required this.todos});
TodoList({@required this.todos, this.onTodoToggle});

final List<Todo> todos;
final ToggleTodoCallback onTodoToggle;

Widget _buildItem(BuildContext context, int index) {
final todo = todos[index];
value: todo.isDone,
title: Text(todo.title),
onChanged: (bool isChecked) {
// _toggleTodo(todo, isChecked);
onTodoToggle(todo, isChecked);
},
);
}
1
2
3
4
5
6
7
8
// todo_list_screen.dart
appBar: AppBar(title: Text('Todo List')),
body: TodoList(
todos: todos,
onTodoToggle: _toggleTodo,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),

至此,这个用 Flutter 编写的 Todo List 应用就基本完成了。

参考链接:

Todo List built with Flutter
flutter设置沉浸式状态栏