使用 Flutter 和 Provider 创建 Todos 应用
Todo 应用一直是初学者学习新知识的理想入门应用。我创建这个应用也完全是为了学习。我使用了provider
package,这是目前Flutter应用内部管理状态的推荐方式。因此,我将向你展示如何使用 Flutter作为状态管理系统,自己创建一个 Todo 应用。provider
您可以在此处找到完成的应用程序。
先决条件
- 对 Flutter 和 Dart 的基本了解
- Flutter 已正确安装在你的系统上
创建和安装依赖项
我们将从创建一个 Flutter 应用开始。您可以使用自己喜欢的 IDE(Android Studio、Intellij IDEA或VS Code)来创建 Flutter 应用。不过,我将通过终端创建该应用。您可以在工作区中运行以下命令来创建新flutter
项目。
$ flutter create todos
创建应用程序后,转到项目目录并打开文件。然后添加我们将用于管理应用程序状态的包pubspec.yaml
的依赖项。您可以从文件中删除该依赖项。在本项目中,我们不会使用该包中的任何内容。provider
cupertino-icons
dependencies:
flutter:
sdk: flutter
provider: ^2.0.1+1
更新文件后,运行以下命令来获取pubspec.yaml
文件中列出的所有包。
$ flutter packages get
上述命令将todos
在你的工作区中创建一个名为 的新目录。现在我们可以开始开发我们的 todos 应用了。
创建模型
我们将在应用中使用单个模型。由于这是一个非常简单的应用,我们只需要创建一个Task,title
并设置它completed
是否可用。
我喜欢让我的项目井井有条。因此,我在lib/models
目录内的一个单独文件中创建了这个模型。我的lib/models/task.dart
文件如下所示。我假设,当completed
参数未传递给构造函数时,它Task
就不完整。
PS:我material
在这里导入了包,以便注释类构造函数所需的参数Todo
。此外,这里
import 'package:flutter/material.dart';
class Task {
String title;
bool completed;
Task({@required this.title, this.completed = false});
void toggleCompleted() {
completed = !completed;
}
}
创建TodosModel
提供商
TodosModel
这是我创建并扩展该类的部分ChangeNotifier
。这是一个provider
特定于包的类。该模型将帮助我们更改应用程序的状态,并在何时重新渲染我们的应用程序或应用程序部分时通知 Flutter。所以,让我们创建一个新文件lib/providers/todos_model.dart
。
这里,我使用UnmodifiableListView
fromdart:collection
来创建 getter。这是为了确保我们的 getter 不能在TodosModel
声明之外以任何方式被操作。
另一个你可能注意到的重要事情是 的频繁使用notifyListeners
。此方法通知 Flutter 状态更改是否需要重新渲染 UI。
PS只有正在监听提供者的 UI 小部件才会重新渲染。
import 'dart:collection';
import 'package:flutter/material.dart';
import 'package:todos/models/task.dart';
class TodosModel extends ChangeNotifier {
final List<Task> _tasks = [];
UnmodifiableListView<Task> get allTasks => UnmodifiableListView(_tasks);
UnmodifiableListView<Task> get incompleteTasks =>
UnmodifiableListView(_tasks.where((todo) => !todo.completed));
UnmodifiableListView<Task> get completedTasks =>
UnmodifiableListView(_tasks.where((todo) => todo.completed));
void addTodo(Task task) {
_tasks.add(task);
notifyListeners();
}
void toggleTodo(Task task) {
final taskIndex = _tasks.indexOf(task);
_tasks[taskIndex].toggleCompleted();
notifyListeners();
}
void deleteTodo(Task task) {
_tasks.remove(task);
notifyListeners();
}
}
最终的 UI
在最后一节中,我将讨论如何布置 UI 以及如何插入我TodosModel
在上一节中创建的内容。
首先让我们看看应用程序的结构。
该应用程序主要由两个屏幕组成。
- 主屏幕
- 添加任务屏幕
主屏幕将再次有一个包含这些选项卡的 TabView
- 所有任务
- 未完成的任务
- 完成任务
HomeScreen
让我们看一下我们必须创建以制作我们的应用程序的小部件的小部件树。
三个标签页将显示类似的小部件。仅根据所选标签页进行筛选。
让我们开始编写我们的TaskListItem
小部件。我们将使用一个ListTile
小部件来创建它。该类TaskListItem
将通过一个实例进行实例化,Task
该实例稍后将被处理并渲染到UI。我在这个位置为这个小部件创建了一个新文件:lib/widgets/task_list_item.dart
。
Provider.of<TodosModel>(context, listen: false)
这里需要注意的另一个新特性是内部onChanged
和参数的使用onPressed
。该provider
包严重依赖于 Dart 的静态类型系统。这里Provider.of<TodosModel>(context, listen: false)
展示了我们稍后将提供给应用程序的实例TodosModel
。然后可以使用此实例调用该类上的任何方法。listen: false
参数告诉 Flutter,此 Widget 在状态更改时无需重新渲染。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todos/models/task.dart';
import 'package:todos/providers/todos_model.dart';
class TaskListItem extends StatelessWidget {
final Task task;
TaskListItem({@required this.task});
@override
Widget build(BuildContext context) {
return ListTile(
leading: Checkbox(
value: task.completed,
onChanged: (bool checked) {
Provider.of<TodosModel>(context, listen: false).toggleTodo(task);
},
),
title: Text(task.title),
trailing: IconButton(
icon: Icon(
Icons.delete,
color: Colors.red,
),
onPressed: () {
Provider.of<TodosModel>(context, listen: false).deleteTodo(task);
},
),
);
}
}
现在,我们将创建一个TaskList
小部件,它将使用之前创建的TaskListItem
部件来显示小部件内的任务列表ListView
。目前,我没有提供任何占位符Text
或任何指示空列表的内容,但您可以继续在此处插入一个新的小部件,以告知用户当前TaskList
小部件为空。
import 'package:flutter/material.dart';
import 'package:todos/models/task.dart';
import 'package:todos/widgets/task_list_item.dart';
class TaskList extends StatelessWidget {
final List<Task> tasks;
TaskList({@required this.tasks});
@override
Widget build(BuildContext context) {
return ListView(
children: getChildrenTasks(),
);
}
List<Widget> getChildrenTasks() {
return tasks.map((todo) => TaskListItem(task: todo)).toList();
}
}
现在,我们可以为HomeScreen
小部件创建所有必要的选项卡了。我们将把小部件的选项卡保存HomeScreen
在一个单独的目录中。我们先从创建小部件开始吧AllTasksTab
。( lib/tabs/all_tasks.dart
)
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todos/providers/todos_model.dart';
import 'package:todos/widgets/task_list.dart';
class AllTasksTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer<TodosModel>(
builder: (context, todos, child) => TaskList(
tasks: todos.allTasks,
),
),
);
}
}
Consumer
是此软件包提供的一个新小部件provider
。此小部件提供了一种简单的方法来监听提供程序状态的变化并相应地重新渲染。通常认为,将庞大的小部件树封装在Consumer
小部件中是一种不好的做法。此小部件应尽可能深入地插入到小部件树中,以避免不必要的重新渲染。更多信息,请参阅此处。
如果任何任务项发生变化,我们需要重新渲染所有列表项。这就是为什么我将小部件的使用TaskList
封装在 中Consumer
。现在,每当我们的提供程序notifyListener
在其模型中调用时,它都会重新渲染我们的TaskList
小部件。
同样,在继续之前,尝试创建剩下的两个选项卡小部件。为了以防万一,我在下面给出了我的代码。
// lib/tabs/completed_tasks.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todos/providers/todos_model.dart';
import 'package:todos/widgets/task_list.dart';
class CompletedTasksTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer<TodosModel>(
builder: (context, todos, child) => TaskList(
tasks: todos.completedTasks,
),
),
);
}
}
// lib/tabs/incomplete_tasks.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todos/providers/todos_model.dart';
import 'package:todos/widgets/task_list.dart';
class IncompleteTasksTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Consumer<TodosModel>(
builder: (context, todos, child) => TaskList(
tasks: todos.incompleteTasks,
),
),
);
}
}
现在我们可以创建我们的HomeScreen
小部件了。这将是一个相当简单的小部件,它将使用我们之前为应用程序创建的小部件。
我们来看看代码:
import 'package:flutter/material.dart';
import 'package:todos/tabs/all_tasks.dart';
import 'package:todos/tabs/completed_tasks.dart';
import 'package:todos/tabs/incomplete_tasks.dart';
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen>
with SingleTickerProviderStateMixin {
TabController controller;
@override
void initState() {
super.initState();
controller = TabController(length: 3, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todos'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
},
),
],
bottom: TabBar(
controller: controller,
tabs: <Widget>[
Tab(text: 'All'),
Tab(text: 'Incomplete'),
Tab(text: 'Complete'),
],
),
),
body: TabBarView(
controller: controller,
children: <Widget>[
AllTasksTab(),
IncompleteTasksTab(),
CompletedTasksTab(),
],
),
);
}
}
应用现在应该可以正确显示所有、已完成和未完成的任务,并且您应该能够切换任务completed
属性的状态。但我们还无法测试应用。我们需要一些演示任务来检查应用是否正常运行。让我们添加一些。
打开您的provider/todos_model.dart
文件并将一些模型实例添加Task
到_tasks
属性中。
final List<Task> _tasks = [
Task(title: 'Finish the app'),
Task(title: 'Write a blog post'),
Task(title: 'Share with community'),
];
现在开始运行你的应用吧。我们创建的所有任务都尚未完成。尝试点击复选框来切换它们。我们的应用现在应该可以正常运行了。你应该能够通过 UI 更新、删除我们创建的任务。现在,我们需要做的最后一件事是创建一个用于将任务添加到应用的屏幕。我们将创建一个简单的AddTaskScreen
状态小部件来为用户提供此功能。我们使用状态小部件是因为在创建新任务时,我们需要获取该小部件的值TextField
以及Checkbox
来自该小部件的小部件。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todos/providers/todos_model.dart';
import 'package:todos/models/task.dart';
class AddTaskScreen extends StatefulWidget {
@override
_AddTaskScreenState createState() => _AddTaskScreenState();
}
class _AddTaskScreenState extends State<AddTaskScreen> {
final taskTitleController = TextEditingController();
bool completedStatus = false;
@override
void dispose() {
taskTitleController.dispose();
super.dispose();
}
void onAdd() {
final String textVal = taskTitleController.text;
final bool completed = completedStatus;
if (textVal.isNotEmpty) {
final Task todo = Task(
title: textVal,
completed: completed,
);
Provider.of<TodosModel>(context, listen: false).addTodo(todo);
Navigator.pop(context);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Add Task'),
),
body: ListView(
children: <Widget>[
Padding(
padding: EdgeInsets.all(15.0),
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(controller: taskTitleController),
CheckboxListTile(
value: completedStatus,
onChanged: (checked) => setState(() {
completedStatus = checked;
}),
title: Text('Complete?'),
),
RaisedButton(
child: Text('Add'),
onPressed: onAdd,
),
],
),
),
)
],
),
);
}
}
我们再次使用包中Provider.of<TodosModel>(context, listen: false)
的provider
来调用addTodo
上的方法TodosModel
。这确保我们的应用状态发生变化,并且所有监听的小部件都会收到此变化的通知并重新渲染。
现在,我们需要做的就是把这个屏幕和我们的HomeScreen
小部件连接起来,瞧!开始吧。打开你的lib/screens/home_screen.dart
文件,更新IconButton
WidgetsonPressed
参数,添加它。
import 'package:flutter/material.dart';
import 'package:todos/screens/add_task_screen.dart';
...
...
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => AddTaskScreen(),
),
);
},
...
...
AddTaskScreen
每当用户在 上按下+按钮时,系统都会将用户带到Appbar
。现在,我们需要做的就是将主应用程序包装到lib/main.dart
一个ChangeNotifierProvider
窗口小部件中,该窗口小部件会将我们的TodosModel
实例传递给应用程序内的所有窗口小部件。lib/main.dart
如下所示更新您的文件:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:todos/screens/home_screen.dart';
import 'package:todos/providers/todos_model.dart';
void main() => runApp(TodosApp());
class TodosApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
builder: (context) => TodosModel(),
child: MaterialApp(
title: 'Todos',
theme: ThemeData.dark(),
home: HomeScreen(),
),
);
}
}
现在,我们的应用已经准备就绪。恭喜你使用 Flutter 和 Provider 创建了你的第一个 Todo 应用。
感谢阅读这篇文章。我提供了一些资源,您可以从中了解有关provider
with 包用法的更多信息flutter
。
本文最初发表在我的个人网站上。