Flutter 中的 State
基本术语中的 State 是应用程序当前实例的描述。为了实现动态页面和应用程序,我们根据需要一次又一次地重建 State 。 Flutter 非常高效,可以在给定时间处理多个 State 更新。但是,为了使用最少的资源,提供了完整的包部分来实现 Flutter 中的 State 管理。
State 是可以在构建 widget 时同步读取的信息,并且可能在 widget 的生命周期内发生变化。widget 实现者有责任使用 State.setState 确保在此类 State 更改时及时通知 State 。
Flutter 中 State 管理的传统解决方案是 setState,但是它有一些缺点,例如深度耦合、昂贵的重建等。还有其他提供简单有效的 State 管理的包,如 GetX、BLoC、RiverPod、Provider 等。
传统实现
import 'package:change_notifier_example/widgets.dart';
import 'package:flutter/material.dart';
class TraditionalSolution extends StatefulWidget {
const TraditionalSolution({Key? key}) : super(key: key);
@override
State<TraditionalSolution> createState() => _TraditionalSolutionState();
}
class _TraditionalSolutionState extends State<TraditionalSolution> {
// 1
List<String> todoList = <String>[];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar("Traditional Implementation"),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 2
todoList.add("Random Value");
setState(() {});
},
child: const Icon(Icons.add),
),
body: ListView.builder(
itemBuilder: (context, index) {
// 3
return getListTile(todoList, index);
},
itemCount: todoList.length,
),
);
}
import 'package:flutter/material.dart';
AppBar getAppBar(String title) {
return AppBar(
centerTitle: true,
elevation: 0,
title: Text(title),
);
}
ListTile getListTile(List<String> items, int index) {
return ListTile(
leading: CircleAvatar(child: Text(index.toString())),
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
title: Text(items[index]),
);
}
}
- 首先,我们在内部创建一个 List< String > 来存储我们的数据。请注意,Stateful Widget 包含列表的声明,现在它是该 Stateful Widget 的私有状态。现在如果我们想在这个页面之外使用这个列表,我们不能使用它,因为它绑定到 widget 的生命周期。
- 在 FloatingActionButton 的 onPressed 内部,我们将另一个项目添加到待办事项列表中,并调用 setState 来更新当前状态。在这里,需要注意的是,我们不能将用户重定向到另一个页面来添加项目。这是因为一旦我们离开这个页面,待办事项列表就会被处理掉。
- 在这里,我们只是在 ListView 中显示列表的内容。
这种类型的状态称为 Ephemeral State,它绑定到单个页面。虽然此代码能够添加和显示项目,但它不是动态的。它只有一页,不能跨页持久化/保存数据。而在大多数现实世界的应用程序中,我们将数据保留在屏幕上,以便提供各种功能,例如修改、删除、添加等。这种跨越两个页面的状态称为应用程序状态。
使用全局变量提升状态
正如我们刚刚看到的,我们的列表绑定到 Widget 的生命周期。为了以一种直接的方式解决这个问题,我们可以做的是将我们的 List 声明为一个全局变量并在其他各种屏幕上访问它。这个实现也有一些缺点,我们将在实现后研究:
import 'package:flutter/material.dart';
class TraditionalSolution extends StatefulWidget {
const TraditionalSolution({Key? key}) : super(key: key);
@override
State<TraditionalSolution> createState() => _TraditionalSolutionState();
}
//? Global Variable
List<String> todoList = <String>[];
class _TraditionalSolutionState extends State<TraditionalSolution> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar("Traditional Implementation"),
floatingActionButton: FloatingActionButton(
onPressed: () {
// 2
Navigator.of(context)
.push(MaterialPageRoute(
builder: (context) => const AddPage(),
))
.then((value) => setState(() {}));
},
child: const Icon(Icons.add),
),
body: ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
// 3
Navigator.of(context)
.push(MaterialPageRoute(
builder: (context) => ModifyPage(index: index),
))
.then((value) => setState(() {}));
},
child: getListTile(todoList, index),
);
},
itemCount: todoList.length,
),
);
}
}
class AddPage extends StatelessWidget {
const AddPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: "Default Text");
return Scaffold(
appBar: getAppBar("Add ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
todoList.add(textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
class ModifyPage extends StatelessWidget {
const ModifyPage({
Key? key,
required this.index,
}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: todoList[index]);
return Scaffold(
appBar: getAppBar("Update ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
todoList[index] = textEditingController.text;
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
AppBar getAppBar(String title) {
return AppBar(
centerTitle: true,
elevation: 0,
title: Text(title),
);
}
ListTile getListTile(List<String> items, int index) {
return ListTile(
leading: CircleAvatar(child: Text(index.toString())),
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
title: Text(items[index]),
);
}
- 将列表声明为全局变量,以便其生命周期不受 widget 生命周期的限制,并且可以从任何地方访问。
- 导航到 AddPage 并且每当我们返回 HomePage 时都会调用 setState。
- 导航到 ModifyPage,当我们返回时调用 setState 来更新 UI。
这个实现看起来是动态的,因为它有 3 个页面,并且在任何这些页面上进行更改时,这些更改也会反映在 HomePage 上。然而,这个实现有一些地方是错误的:
- HomePage、AddPage 和 ModifyPage 之间的重度耦合。
- 如果用户不添加或修改数据,我们仍然在 HomePage 上调用 setState。
- UI 是动态的,但不是反应式的。
- 这个实现就像是在 Flutter 框架下工作。在 Flutter 中,最好将所需的依赖项注入到 widget 树中。
Flutter 中的 ChangeNotifier
ChangeNotifier 是一个类,它在我们想要通知其更改时向其侦听器提供通知。这意味着可以订阅扩展 ChangeNotifier 的类,并在类发生更改时调用其 notifyListeners() 方法。此调用将通知所有附加的侦听器。 ChangeNotifier 是 Flutter 原生的,主要与 Provider 和 RiverPod 包一起使用。
ValueNotifier 是一个带有单个值的 ChangeNotifier,它会在其 value 属性更改时通知其侦听器。但是,它只包含一个值,当我们使用扩展 ChangeNotifier 的类时,我们可以在其中定义多个值。
class MyValueNotifier extends ValueNotifier<int>{
// Only holds single value
int getValue() => value;
}
class MyChangeNotifier extends ChangeNotifier{
// Can hold multiple values
}
现在让我们为我们的 TODO 应用程序实现 ChangeNotifier:
import 'package:flutter/material.dart';
import 'dart:collection';
class ItemNotifier extends ChangeNotifier {
final List<String> _items = <String>[];
int _size = 0;
List<String> getItems() => UnmodifiableListView(_items);
int getSize() => _size;
void add(String value) {
_items.add(value);
_size++;
notifyListeners();
}
void delete(int index){
_items.removeAt(index);
_size--;
notifyListeners();
}
void modify(int index, String data){
_items[index] = data;
notifyListeners();
}
}
Flutter 中使用 ChangeNotifier 的方法
我们有几种方法可以在 Flutter 中使用我们的变更通知器。
- 使用 .addListener 方法,因为 ChangeNotifier 是一种 Listenable。
- 另一种方法是使用 AnimatedBuilder 使用 ChangeNotifier,因为它也需要一个 Listenable。
- 最后,我们可以使用 ChangeNotifierProvider、Consumer 和 Provider 的组合。所有这些功能都是由 Provider 包提供给我们的。
第一种和第二种方法依赖于在全局范围内声明 ChangeNotifier,而第三种方法将依赖类注入到 Widget Tree 中,以便我们可以在任何地方访问它。
在开始实施之前,只是为了确保它不会变得混乱。请记住,我们有 3 个页面,HomePage、AddPage 和 ModifyPage。每个实现都将包含自己的 AddPage 和 ModifyPage。另外,请注意 getAppBar 和 getListTile 函数是在一个名为 widgets.dart 的类中声明的。这些函数是单独声明的,因为它们在所有实现中都是相同的。
# lib/widgets.dart
import 'package:flutter/material.dart';
AppBar getAppBar(String title) {
return AppBar(
centerTitle: true,
elevation: 0,
title: Text(title),
);
}
ListTile getListTile(List<String> items, int index) {
return ListTile(
leading: CircleAvatar(child: Text(index.toString())),
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
title: Text(items[index]),
);
}
使用 .addListener((){})
class ListenChangeNotifier extends StatefulWidget {
const ListenChangeNotifier({Key? key}) : super(key: key);
@override
State<ListenChangeNotifier> createState() =>
_ListenChangeNotifierState();
}
ItemNotifier itemNotifier = ItemNotifier();
class _ListenChangeNotifierState extends State<ListenChangeNotifier> {
@override
void initState() {
super.initState();
// 2
itemNotifier.addListener(() => mounted ? setState(() {}) : null);
}
@override
void dispose() {
// 3
itemNotifier.removeListener(() {});
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar("Default Change Notifier Example"),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const AddPage()),
),
child: const Icon(Icons.add),
),
body: _getListView(),
);
}
ListView _getListView() {
return ListView.builder(
itemCount: itemNotifier.getSize(),
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ModifyPage(index: index)),
),
child: getListTile(itemNotifier.getItems(), index),
);
},
);
}
}
class AddPage extends StatelessWidget {
const AddPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: "Default Text");
return Scaffold(
appBar: getAppBar("Add ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
itemNotifier.add(textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
class ModifyPage extends StatelessWidget {
const ModifyPage({
Key? key,
required this.index,
}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: itemNotifier.getItems()[index]);
return Scaffold(
appBar: getAppBar("Update ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
itemNotifier.modify(index, textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
这种方法还将 ItemNotifier 声明为全局变量。但是,请注意,我们仅通过向 ItemNotifier 添加侦听器来侦听有效事件。当我们手动添加侦听器时,必须在不需要时将其丢弃。但是,在这种方法中,我们没有将依赖项注入到 widget 树中,它与全局范围内的 widget 树不同。
使用 AnimatedBuilder 消费
正如我们所讨论的,AnimatedBuilder 接受任何 Listenable 作为参数。 ChangeNotifier 类也扩展了 Listenable,因此我们可以简单地使用 AnimatedBuilder。
import 'package:flutter/material.dart';
import 'dart:collection';
class ItemNotifier extends ChangeNotifier {
final List<String> _items = <String>[];
int _size = 0;
List<String> getItems() => UnmodifiableListView(_items);
int getSize() => _size;
void add(String value) {
_items.add(value);
_size++;
notifyListeners();
}
void delete(int index){
_items.removeAt(index);
_size--;
notifyListeners();
}
void modify(int index, String data){
_items[index] = data;
notifyListeners();
}
}
class AnimatedChangeNotifierExample extends StatefulWidget {
const AnimatedChangeNotifierExample({Key? key}) : super(key: key);
@override
State<AnimatedChangeNotifierExample> createState() =>
_AnimatedChangeNotifierExampleState();
}
// 1
ItemNotifier itemNotifier = ItemNotifier();
class _AnimatedChangeNotifierExampleState
extends State<AnimatedChangeNotifierExample> {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
// 2
animation: itemNotifier,
builder: (context, child) {
return Scaffold(
appBar: getAppBar("Animated Change Notifier Example"),
floatingActionButton: FloatingActionButton(
onPressed: () => itemNotifier.add("Random Name"),
child: const Icon(Icons.add),
),
body: ListView.builder(
itemBuilder: (context, index) {
return getListTile(itemNotifier.getItems(), index);
},
itemCount: itemNotifier.getSize(),
),
);
},
);
}
}
class AddPage extends StatelessWidget {
const AddPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: "Default Text");
return Scaffold(
appBar: getAppBar("Add ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
itemNotifier.add(textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
class ModifyPage extends StatelessWidget {
const ModifyPage({
Key? key,
required this.index,
}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: itemNotifier.getItems()[index]);
return Scaffold(
appBar: getAppBar("Update ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
itemNotifier.modify(index, textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
AppBar getAppBar(String title) {
return AppBar(
centerTitle: true,
elevation: 0,
title: Text(title),
);
}
ListTile getListTile(List<String> items, int index) {
return ListTile(
leading: CircleAvatar(child: Text(index.toString())),
contentPadding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0),
title: Text(items[index]),
);
}
这里我们也声明了全局的 ItemNotifier,它类似于监听,但是这里我们不必手动调用setState。 AnimatedBuilder 在内部管理订阅,因此我们不需要在这里处理 ItemNotifier 监听器。
我们讨论的两种方法都没有将依赖注入到 widget 树中,依赖存在于全局范围内。现在我们来看看 Flutter 中的 ChangeNotifierProvider 和 Provider。它们提供强大的依赖注入,使得依赖在整个 widget 树中都可用。
Flutter 中的 ChangeNotifierProvider
ChangeNotifierProvider 旨在为 Flutter 中的 ChangeNotifier 提供更精细的用例。像 AnimatedBuilder、StreamBuilder、FutureBuilder、ValueListenableBuilder 等其他构建器一样。它就像一个接受 ChangeNotifier 并更新其任何值更改的子级的构建器。
要在 Flutter 中使用 ChangeNotifierProvider,我们需要将 Provider 依赖添加到我们的 pubspec.yaml 中。
添加最新的 Provider 依赖:
provider: ^6.0.2
在我们的例子中,ChangeNotifierProvider< T > 包裹在 MaterialApp 周围。请不要使用下层 Widget Tree 中不需要的依赖项污染范围。在我们的例子中,我们只有 3 个页面,并且都需要访问相同的依赖项,因此为了简单起见,我们将其包装在 MaterialApp 中。
依赖注入
ChangeNotifierProvider(
create: (context) => ItemNotifier(),
child: MaterialApp(...),
);
正如看到的那样,它也遵循单例模式,实例在 create 方法中创建一次,并且可供底层 widget 使用。 ChangeNotifierProvider 是 Flutter 中 InheritedWidgets 的简化版。在这里,我们已经将 ItemNotifier 注入到 Widget Tree 中,现在我们可以在我们的应用程序中访问它。
我们还可以使用 MultiProvider 将多个依赖项注入到 Widget Tree 中:
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => ItemNotifier()),
Provider(create: (context) => SomeOtherClass()),
],
child: const MyApp(),
);
请注意,ChangeNotifierProvider 接受一个 ChangeNotifier 类,而 Provider 接受一个简单的类。 Provider 用于我们想要共享值但不想更新侦听器的情况。
访问我们的依赖
有两种方法可以访问我们的依赖项,一种是使用 Consumer< T > 小部件,另一种是使用 Provider.of< T >(context)。
- 使用 Consumer< T > 当我们想要在值更改时重建 widget 时,我们使用 Consumer< T >。必须提供类型 < T > 以便 Provider 可以理解您所指的依赖项。
Consumer<ItemNotifier>(
builder: (context, value, child) {...},
child: SomeWidget(),
);
Consumer widget 有两个参数,builder 参数是必需的,child 参数是可选的。 child 参数是不受 ChangeNotifier 中任何更改影响的任何昂贵的 widget 。
- 使用 Provider.of< T >(context) 当您需要访问依赖项但不想对用户界面进行任何更改时,将使用 Provider.of< T >(context)。我们可以使用 Consumer< T > ,但这会浪费资源。我们只是将监听设置为 false,表示我们不需要监听来自 ChangeNotifier 的更新。
Provider.of<ItemNotifier>(context, listen: false).delete(0);
我们在需要更新 UI 的地方使用 Consumer。 Provider.of(context) 用于我们不需要任何关于所做更改的进一步通知的地方,因此我们将 listen 参数设置为 false 并使用 ItemNotifier 类中提供的函数。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:change_notifier_example/widgets.dart';
import 'data/item_notifier.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => ItemNotifier(),
child: MaterialApp(
home: const ChangeNotifierExample(),
),
);
}
}
class ChangeNotifierExample extends StatefulWidget {
const ChangeNotifierExample({Key? key}) : super(key: key);
@override
State<ChangeNotifierExample> createState() => _ChangeNotifierExampleState();
}
class _ChangeNotifierExampleState extends State<ChangeNotifierExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: getAppBar("ChangeNotifier Builder"),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const AddPage()),
),
child: const Icon(Icons.add),
),
body: Consumer<ItemNotifier>(builder: (context, value, child) {
return ListView.builder(
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ModifyPage(index: index)),
),
child: getListTile(value.getItems(), index),
);
},
itemCount: value.getSize(),
);
}),
);
}
}
class AddPage extends StatelessWidget {
const AddPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
TextEditingController textEditingController =
TextEditingController(text: "Default Text");
return Scaffold(
appBar: getAppBar("Add ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<ItemNotifier>(context, listen: false)
.add(textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
class ModifyPage extends StatelessWidget {
const ModifyPage({
Key? key,
required this.index,
}) : super(key: key);
final int index;
@override
Widget build(BuildContext context) {
TextEditingController textEditingController = TextEditingController(
text: Provider.of<ItemNotifier>(context, listen: false).getItems()[index],
);
return Scaffold(
appBar: getAppBar("Update ToDoItem"),
floatingActionButton: FloatingActionButton(
onPressed: () {
Provider.of<ItemNotifier>(context, listen: false)
.modify(index, textEditingController.text);
Navigator.pop(context);
},
child: const Icon(Icons.add),
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0),
child: TextField(
controller: textEditingController,
),
),
),
);
}
}
结论
在这篇文章中,我们了解了 ChangeNotifier 和 ChangeNotifierProvider 的基础知识。我们讨论了如何在 Flutter 中创建和使用 ChangeNotifier。 ChangeNotifer 是 Flutter 的原生功能,即您无需添加任何依赖项即可使用它,但它通常与 Provider 一起使用以提供高级功能。