为什么不在 Flutter 中使用全局变量来管理状态
bigegpt 2025-07-14 14:58 4 浏览
我相信没有人用全局变量来管理 Flutter 应用程序的状态。 毫无疑问,我们的 Flutter 应用程序需要状态管理包或 Flutter 的基本小部件(例如 InheritedWidget 或 StatefulWidget)。
然而,我们真的了解这些包对于状态管理有何价值吗? 在思考状态管理时,我们必须考虑哪些主题?
在本文中,我们将有意使用全局变量进行状态管理来构建一个简单的计数器应用程序,以探索会发生什么样的问题。 这一挑战将帮助我们了解状态管理包试图解决什么问题。
具有全局变量整数值的计数器应用程序
我们将从下面的简单计数器应用程序开始。
import 'package:flutter/material.dart';
void main() => runApp(const MainApp());
var counter = 0;
class MainApp extends StatelessWidget {
const MainApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text(counter.toString()),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () {
counter++;
},
),
),
);
}
}
它必须是一个简单的计数器应用程序,除了被声明为全局变量的计数器之外。
当然,即使我们点击 FloatingActionButton,Text 也始终显示 0,因为它只会增加计数,而不会导致重建。
了解重建机制
在我们继续让应用程序运行之前,讨论重建如何发生对于理解以下讨论非常重要。
我们首先需要了解所有小部件在构建阶段都会创建其相应的元素。 Element 管理是否需要在下一帧中使用名为 _dirty 的标志重建相应的小部件。
由于 _dirty 是通过 markNeedsBuild 方法修改的,所以我们现在需要的是在点击 FloatingActionButton 时调用该方法来重建计数器应用程序。
StatefulWidget的setState
调用markNeedsBuild最简单的方法是调用StatefulWidget的setState。
StatefulWidget的State和Element一样,也是由StatefulWidget创建的,与Element和Widget构建一对一的关系。 setState 通过调用相应 Element 的 markNeedsBuild 来重建相应的 StatefulWidget,如下代码所示。
@protected
void setState(VoidCallback fn) {
final Object? result = fn() as dynamic;
_element!.markNeedsBuild();
}
因此,我们需要将MainApp设为StatefulWidget,并在FloatingActionButton的onPressed中调用setState。
onPressed: () {
setState(() => counter++);
},
伟大的! 我们使计数器应用程序按预期工作。
管理应用程序状态
我们解决了所有问题吗? 当然不是。
当我们与多个小部件共享状态时,会出现另一个问题。
让我们创建另一个名为 FirstPage 的屏幕小部件,并将现有小部件提取为 SecondPage。 两个页面都在屏幕中央显示计数,但 FirstPage 的 FloatingActionButton 用于导航到 SecondPage,而 SecondPage 的 FloatingActionButton 用于递增计数器。
您可以在此处查看差异以及下面的演示。
根据到目前为止的讨论,FirstPage 始终显示 0 的原因很清楚; FirstPage 的 markNeedsBuild 从未被调用。
FirstPage 肯定指的是状态计数器并首先显示其值,但永远不知道其更新。 换句话说,能够引用状态对于状态管理来说是不够的,但接收这些更新也是强制性的。
ValueNotifier 和 ValueListenableBuilder
我们的下一个挑战是解决 FirstPage 无法注意到计数器更新的问题。 为了解决这个问题,我们有 ValueNotifier 和 ValueListenableBuilder。
ValueNotifier 通知保存值的更新,ValueListenableBuilder 是一个用于接收 ValueNotifier 通知并调用 setStateinside 的 widget。
首先,我们将计数器变量更新为扩展 ValueNotifier 的名为 Counter 的类。
final counter = Counter();
class Counter extends ValueNotifier<int> {
Counter() : super(0);
void increment() => value++;
}
它还公开了用于增加值的方法increment,因为值不能直接从小部件更新。
接下来,我们使用 ValueListenableBuilder 包装 FirstPage 的文本,如下所示。
child: ValueListenableBuilder(
valueListenable: counter,
builder: (context, value, child) {
return Text(value.toString());
},
),
最后,我们可以在第一页和第二页之间完美地共享计数器。 检查这里的全部差异和下面的演示。
组合多个状态
那么,我们现在解决所有问题了吗? 当然,仍然没有。
我们正在解决下一个组合多个状态的问题。 我们希望 MultipliedCounter 暴露计数器值两倍的值。 我们现在必须发明一种方法来捕获小部件之外的 Counter 通知,这意味着不需要 ValueListenableBuilder。
这是监听其他 ValueNotifier 更新的技巧。
final multipleCounter = MultipliedCounter(counter);
class MultipliedCounter extends ValueNotifier<int> {
MultipliedCounter(Counter counter) : super(0) {
counter.addListener(() {
multiply(counter.value);
});
}
void multiply(int base) => value = base * 2;
}
因为MultipliedCounter要做的就是向Counter添加一个监听器,它通过构造函数接收Counter对象,并调用ValueNotifier提供的addListener,传递调用multiply方法的函数。 这个技巧使 MultipleCounter 能够接收 Counter 的更新,并使用 base * 2 更新其值。
以下是在 FirstPage 中使用 multipliedCounter 的示例。
处置状态
我们还有大量的问题没有解决,其中之一就是如何处置状态。
一般来说,当我们不再需要状态时,我们必须处置它们,但是我们的 counter 或 multipliedCounter 永远不会自动处置,因为它们被定义为全局变量,只要我们的应用程序处于活动状态,它们就会保持活动状态。
如果我们希望在不再使用它们时将其处理掉,则必须通过调用某种方法来手动处理它们。
我们必须意识到,在这种情况下,分配一个全新的对象来计数器是完全无关的。
// don't do this for disposing
counter = Counter();
由于 multipliedCounter 或 ValueListenableBuilder 已经引用了现有的 Counter 对象,因此将新的 Counter 对象分配给 counter 变量不会导致自动切换它们的引用。
然后,如何提供方法initialize(dispose已在ValueNotifier中定义)并在相关时间调用它,例如在_SecondPageState的dispose中?
class Counter extends ValueNotifier<int> {
Counter() : super(0);
void increment() => value++;
void initialize() => value = 0; // provide a method to initialize
}
@override
void dispose() {
counter.initialize();
super.dispose();
}
但是等等,在这里调用初始化是否安全? 我们必须记住,multipliedCounter 也在监听 counter,而 _SecondPageState 永远不知道这个事实,反之亦然。 换句话说,没有人知道“计数器不再被任何人使用”。
那么,我们首先要做的就是建立听抗辩的管理机制,但是如何建立呢?
这是一个相当困难的问题,我们在本文中不会回答它。
测试
最后但不是唯一的问题是测试。
当我们考虑使用小部件测试来测试 SecondPage 时,我们很快发现它不起作用。
void main() {
testWidgets('SecondPage shows 0 first and 1 after tapping button', (widgetTester) async {
await widgetTester.pumpWidget(const MaterialApp(home: SecondPage()));
expect(find.text('0'), findsOneWidget);
final incrementButton = find.byIcon(Icons.add);
await widgetTester.tap(incrementButton);
await widgetTester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
}
看看这个简单的小部件测试,它似乎通过了所有测试,而且确实如此。
$ flutter test test/second_page_test.dart
00:00 +0: loading /path/to/global_variable_counter/test/second_page_test.dart
00:01 +0: SecondPage
00:01 +1: SecondPage
00:01 +1: All tests passed!
然而,一旦我们在这里添加另一个 testWidgets,如下所示,
void main() {
testWidgets('SecondPage shows 0 first and 1 after tapping button',
(widgetTester) async {
await widgetTester.pumpWidget(const MaterialApp(home: SecondPage()));
expect(find.text('0'), findsOneWidget);
final incrementButton = find.byIcon(Icons.add);
await widgetTester.tap(incrementButton);
await widgetTester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
testWidgets('SecondPage shows 0 first and 1 after tapping button',
(widgetTester) async {
await widgetTester.pumpWidget(const MaterialApp(home: SecondPage()));
expect(find.text('0'), findsOneWidget);
final incrementButton = find.byIcon(Icons.add);
await widgetTester.tap(incrementButton);
await widgetTester.pumpAndSettle();
expect(find.text('1'), findsOneWidget);
});
}
测试显示以下失败。
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
Actual: _TextFinder:<zero widgets with text "0" (ignoring offstage widgets)>
Which: means none were found but one was expected
When the exception was thrown, this was the stack:
...
这是因为全局变量在 main() 中保持活动状态,并且在每个测试中共享相同的对象。 这意味着我们需要在每个 testWidget 之前初始化所有状态。
结论
到目前为止,我们已经尝试构建一个使用全局变量作为其状态管理策略的计数器应用程序,我们发现了很多关于初始化、更新、监视和处理状态的问题。 此外,由于全局变量,测试小部件也变得具有挑战性。
我不相信人们愿意一一解决这些问题。 这是 Riverpod、BLoC、GetX 等状态管理包的业务。
由于这些包为本文讨论的问题提供了解决方案,因此我们不需要浪费时间与它们作斗争。 我们要做的就是了解状态管理存在什么样的问题,以及如何使用包正确解决这些问题。
感谢您阅读这篇文章,我想再写一篇文章来介绍每个状态管理包如何处理问题,以便我们为每个项目找到最佳的状态管理包及其用法。
相关推荐
- 最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)
-
前言作为一名编程人员,对MySQL一定不会陌生,尤其是互联网行业,对MySQL的使用是比较多的。对于求职者来说,MySQL又是面试中一定会问到的重点,很多人拥有大厂梦,却因为MySQL败下阵来。实际上...
- Redis数据库从入门到精通(redis数据库设计)
-
目录一、常见的非关系型数据库NOSQL分类二、了解Redis三、Redis的单节点安装教程四、Redis的常用命令1、Help帮助命令2、SET命令3、过期命令4、查找键命令5、操作键命令6、GET命...
- netcore 急速接入第三方登录,不看后悔
-
新年新气象,趁着新年的喜庆,肝了十来天,终于发了第一版,希望大家喜欢。如果有不喜欢看文字的童鞋,可以直接看下面的地址体验一下:https://oauthlogin.net/前言此次带来得这个小项目是...
- 精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)
-
大家好,我是柠檬哥,专注编程知识分享。欢迎关注@程序员柠檬橙,编程路上不迷路,私信发送以下关键字获取编程资源:发送1024打包下载10个G编程资源学习资料发送001获取阿里大神LeetCode...
- Oracle 12c系列(一)|多租户容器数据库
-
作者杨禹航出品沃趣技术Oracle12.1发布至今已有多年,但国内Oracle12C的用户并不多,随着12.2在去年的发布,选择安装Oracle12c的客户量明显增加,在接下来的几年中,Or...
- flutter系列之:UI layout简介(flutter-ui-nice)
-
简介对于一个前端框架来说,除了各个组件之外,最重要的就是将这些组件进行连接的布局了。布局的英文名叫做layout,就是用来描述如何将组件进行摆放的一个约束。在flutter中,基本上所有的对象都是wi...
- Flutter 分页功能表格控件(flutter 列表)
-
老孟导读:前2天有读者问到是否有带分页功能的表格控件,今天分页功能的表格控件详细解析来来。PaginatedDataTablePaginatedDataTable是一个带分页功能的DataTable,...
- Flutter | 使用BottomNavigationBar快速构建底部导航
-
平时我们在使用app时经常会看到底部导航栏,而在flutter中它的实现也较为简单.需要用到的组件:BottomNavigationBar导航栏的主体BottomNavigationBarI...
- Android中的数据库和本地存储在Flutter中是怎样实现的
-
如何使用SharedPreferences?在Android中,你可以使用SharedPreferencesAPI来存储少量的键值对。在Flutter中,使用Shared_Pref...
- Flet,一个Flutter应用的实用Python库!
-
▼Flet:用Python轻松构建跨平台应用!在纷繁复杂的Python框架中,Flet宛如一缕清风,为开发者带来极致的跨平台应用开发体验。它用最简单的Python代码,帮你实现移动端、桌面端...
- flutter系列之:做一个图像滤镜(flutter photo)
-
简介很多时候,我们需要一些特效功能,比如给图片做个滤镜什么的,如果是h5页面,那么我们可以很容易的通过css滤镜来实现这个功能。那么如果在flutter中,如果要实现这样的滤镜功能应该怎么处理呢?一起...
- flutter软件开发笔记20-flutter web开发
-
flutterweb开发优势比较多,采用统一的语言,就能开发不同类型的软件,在web开发中,特别是后台式软件中,相比传统的html5开发,更高效,有点像c++编程的方式,把web设计出来了。一...
- Flutter实战-请求封装(五)之设置抓包Proxy
-
用了两年的flutter,有了一些心得,不虚头巴脑,只求实战有用,以供学习或使用flutter的小伙伴参考,学习尚浅,如有不正确的地方还望各路大神指正,以免误人子弟,在此拜谢~(原创不易,转发请标注来...
- 为什么不在 Flutter 中使用全局变量来管理状态
-
我相信没有人用全局变量来管理Flutter应用程序的状态。毫无疑问,我们的Flutter应用程序需要状态管理包或Flutter的基本小部件(例如InheritedWidget或St...
- Flutter 攻略(Dart基本数据类型,变量 整理 2)
-
代码运行从main方法开始voidmain(){print("hellodart");}变量与常量var声明变量未初始化变量为nullvarc;//未初始化print(c)...
- 一周热门
- 最近发表
-
- 最全的MySQL总结,助你向阿里“开炮”(面试题+笔记+思维图)
- Redis数据库从入门到精通(redis数据库设计)
- netcore 急速接入第三方登录,不看后悔
- 精选 30 个 C++ 面试题(含解析)(c++面试题和答案汇总)
- Oracle 12c系列(一)|多租户容器数据库
- flutter系列之:UI layout简介(flutter-ui-nice)
- Flutter 分页功能表格控件(flutter 列表)
- Flutter | 使用BottomNavigationBar快速构建底部导航
- Android中的数据库和本地存储在Flutter中是怎样实现的
- Flet,一个Flutter应用的实用Python库!
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)