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() { SystemChrome.setEnabledSystemUIOverlays([]); runApp(MyApp()); }
|
在 flutter 中所有的组件叫做 widget
。widget
既可以是 stateless
也可以是 stateful
。最上层的组件应该是无状态的,所以在这里我们创建一个 statelessWidget
:
1
| class MyApp extends StatelessWidget {}
|
每一个 widget 都应该覆盖 build
方法。该方法返回你的布局 widget(container
,padding
, 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
| class TodoList extends StatefulWidget { @override _TodoListState createState() => _TodoListState(); }
class _TodoListState extends State<TodoList> { @override Widget build(BuildContext context) { return Container(); } }
|
引入 TodoList
widget:
1 2
| import 'package:todo_list/todo_list.dart';
|
将每一项 todo 待办事项抽象为一个类:
1 2 3 4 5 6 7
| class Todo { Todo({this.title, this.isDone = false});
String title; bool isDone; }
|
并且在 todo_list.dart 中引用:
1 2
| import 'package:todo_list/todo.dart';
|
创建一个仅由 Todo
类组成的 list
:
1 2 3
| class _TodoListState extends State<TodoList> { List<Todo> todos = [];
|
使用 ListView
渲染待办事项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
| Widget _buildItem(BuildContext context, int index) { final todo = todos[index]; return CheckboxListTile( ); }
|
Value
表示待办事项是否已完成
1 2 3 4 5 6
| final todo = todos[index]; return CheckboxListTile( value: todo.isDone, ); }
|
Title
表示该 CheckboxListTile
的主要内容
1 2 3 4 5
| return CheckboxListTile( value: todo.isDone, title: Text(todo.title), );
|
最后,我们需要处理每个列表项目的点击
1 2 3 4 5 6 7 8
| return CheckboxListTile( value: todo.isDone, title: Text(todo.title), onChanged: (bool isChecked) { _toggleTodo(todo, isChecked); }, );
|
_toggleTodo
的实现非常简单:
1 2 3 4 5 6
| _toggleTodo(Todo todo, bool isChecked) { setState(() { todo.isDone = isChecked; }); }
|
todo列表项目的渲染和更新已经完成,现在开始实现添加 todo 列表项目的 UI 及功能:
添加一个 FloatingActionButton
1 2 3 4 5 6 7 8 9 10 11
| 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
|
home: TodoList(),
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| _addTodo() {}
@override Widget build(BuildContext context) {
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
| _addTodo() { showDialog( context: context, builder: (BuildContext context) { return AlertDialog( title: Text('New todo'), ); }, ); }
|
在对话框中添加一个文本输入框和两个按钮:Cancel
,Add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| _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
| FlatButton( child: Text('Cancel'), onPressed: () { Navigator.of(context).pop(); }, ),
|
为了获取用户在文本输入框中输入的值以创建 todo 项目,我们需要创建一个 TextEditingController
1 2 3 4 5
| class _TodoListState extends State<TodoList> { List<Todo> todos = [];
TextEditingController controller = new TextEditingController();
|
并且添加到 TextField
1 2 3 4
| return AlertDialog( title: Text('New todo'), content: TextField(),
|
最后,创建新 todo 并将其添加到现有 todo 列表中
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
改进:通过对 TextField
的 autofocus
属性设置为 true
使其自动对焦文本输入框并使键盘自动弹出
1 2 3 4 5 6 7 8
| return AlertDialog( title: Text('New todo'), 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
| _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: () { final todo = new Todo(title: controller.value.text); controller.clear(); Navigator.of(context).pop(todo); }, ), ], );
|
为了能够在 _addTodo
方法中能接收 Todo
类,我们需要使它成为异步并等待 showDialog
函数结果(如果它被解除则为null,否则为Todo的实例)
1 2 3 4 5 6 7 8
|
_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
| ); }, );
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
| 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
| import 'package:todo_list/new_todo_dialog.dart';
return NewTodoDialog();
|
下一步——提炼 todo list 组件
列表部分也可以视为 stateless widget,状态相关的逻辑可以由父级处理:
首先将 TodoList
重命名为 TodoListScreen
1 2 3 4 5 6 7 8 9 10 11 12 13
| import 'package:todo_list/new_todo_dialog.dart';
class TodoListScreen extends StatefulWidget { @override
_TodoListScreenState createState() => _TodoListScreenState(); }
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
| import 'package:flutter/material.dart'; import 'package:flutter/services.dart';
import 'package:todo_list/todo_list_screen.dart';
void main() { SystemChrome.setEnabledSystemUIOverlays([]); Widget build(BuildContext context) { return MaterialApp( title: 'Todo List',
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
| 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
| import 'package:todo_list/todo_list.dart';
_addTodo() async { final todo = await showDialog<Todo>( context: context, Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Todo List')),
body: TodoList(), floatingActionButton: FloatingActionButton( child: Icon(Icons.add), onPressed: _addTodo,
|
现在让我们 review 一下我们的 TodoList widget
别忘了加载 todo 类
1 2
| import 'package:todo_list/todo.dart';
|
它也没有 todos ,所以我们让父部件传递给它
1 2 3 4 5
| class TodoList extends StatelessWidget { TodoList({@required this.todos});
final List<Todo> todos;
|
1 2 3 4 5 6
| appBar: AppBar(title: Text('Todo List')),
body: TodoList( todos: todos, ),
|
_toggleTodo
方法依赖 setState,所以我们将它移回父级 widget
1 2 3 4 5 6 7 8
| 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
| import 'package:todo_list/todo.dart';
typedef ToggleTodoCallback = void Function(Todo, bool); class TodoList extends StatelessWidget { 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) { onTodoToggle(todo, isChecked); }, ); }
|
1 2 3 4 5 6 7 8
| 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设置沉浸式状态栏