共计 6991 个字符,预计需要花费 18 分钟才能阅读完成。
文 / Nayuta,CFUG 社区
状态治理始终是 Flutter 开发中一个炽热的话题。谈到状态治理框架,社区也有诸如有以
Get、Provider
为代表的多种计划,它们有各自的优缺点。
面对这么多的抉择,你可能会想:「我须要应用状态治理么?哪种框架更适宜我?」
本文将从作者的理论开发教训登程,剖析状态治理解决的问题以及思路,心愿能帮忙你做出抉择。
为什么须要状态治理?
首先,为什么须要状态治理?
依据笔者的教训,这是因为 Flutter 基于
申明式 构建 UI,
应用状态治理的目标之一就是解决「申明式」开发带来的问题。
「申明式」开发是一种区别于传原生的形式,所以咱们没有在原生开发中听到过状态治理,那如何了解「申明式」开发呢?
「申明式」VS「命令式」剖析
以最经典的的计数器例子剖析:
如上图所示:点击右下角按钮,显示的文本数字加一。
Android 中能够这么实现:当右下角按钮点中时,
拿到 TextView
的对象,手动设置其展现的文本。
实现代码如下:
// 一、定义展现的内容
private int mCount =0;
// 二、两头展现数字的控件 TextView
private TextView mTvCount;
// 三、关联 TextView 与 xml 中的组件
mTvCount = findViewById(R.id.tv_count)
// 四、点击按钮管制组件更新
private void increase( ){
mCount++;
mTvCounter.setText(mCount.toString());
}
而在 Flutter 中,咱们只须要使变量减少之后调用 setState((){})
即可。setState
会刷新整个页面,使得两头展现的值进行变更。
// 一、申明变量
int _counter =0;
// 二、展现变量
Text('$_counter')
// 三、变量减少,更新界面
setState(() {_counter++;});
能够发现,Flutter 中只对 _counter
属性进行了批改,并没有对 Text 组件进行任何的操作,整个界面随着状态的扭转而扭转。
所以在 Flutter 中有这么一种说法: UI = f(state):
下面的例子中,状态 (state) 就是 _counter
的值,调用 setState
驱动 f
build 办法生成新的 UI。
那么,申明式有哪些劣势,并带来了哪些问题呢?
劣势: 让开发者解脱组件的繁琐管制,聚焦于状态解决
习惯 Flutter 开发之后,回到原生平台开发,你会发现当多个组件之间互相关联时,对于 View 的管制十分麻烦。
而在 Flutter 中咱们只须要解决好状态即可 (复杂度转移到了状态 -> UI 的映射,也就是 Widget 的构建)。包含 Jetpack Compose、Swift 等技术的最新倒退,也是在朝着「申明式」的方向演进。
申明式开发带来的问题
没有应用状态治理,间接「申明式」开发的时候,遇到的问题总结有三个:
- 逻辑和页面 UI 耦合,导致无奈复用 / 单元测试、批改凌乱等
- 难以跨组件 (跨页面) 拜访数据
- 无奈轻松的管制刷新范畴 (页面 setState 的变动会导致全局页面的变动)
接下来,我先率领大家一一理解这些问题,下一章向大家详细描述状态治理框架如何解决这些问题。
1) 逻辑和页面 UI 耦合,导致无奈复用 / 单元测试、批改凌乱等
一开始业务不简单的时候,所有的代码都间接写到 widget 中,随着业务迭代,
文件越来越大,其余开发者很难直观地明确外面的业务逻辑。
并且一些通用逻辑,例如网络申请状态的解决、分页等,在不同的页面来回粘贴。
这个问题在原生上同样存在,前面也衍生了诸如 MVP 设计模式的思路去解决。
2) 难以跨组件 (跨页面) 拜访数据
第二点在于跨组件交互,比方在 Widget 构造中,
一个子组件想要展现父组件中的 name
字段,
可能须要层层进行传递。
又或者是要在两个页面之间共享筛选数据,
并没有一个很优雅的机制去解决这种跨页面的数据拜访。
3) 无奈轻松的管制刷新范畴 (页面 setState 的变动会导致全局页面的变动)
最初一个问题也是下面提到的长处,很多场景咱们只是局部状态的批改,例如按钮的色彩。
然而整个页面的 setState
会使得其余不须要变动的中央也进行重建,
带来不必要的开销。
Provider、Get 状态治理框架设计剖析
Flutter 中状态治理框架的外围在于这三个问题的解决思路,
上面一起看看 Provider、Get 是如何解决的:
解决逻辑和页面 UI 耦合问题
传统的原生开发同样存在这个问题,Activity 文件也可能随着迭代变得难以保护,
这个问题能够通过 MVP 模式进行解耦。
简略来说就是将 View 中的逻辑代码抽离到 Presenter 层,
View 只负责视图的构建。
这也是 Flutter 中简直所有状态治理框架的解决思路,
上图的 Presenter 你能够认为是 Get 中的 GetController
、
Provider 中的 ChangeNotifier
或者 Bloc 中的 Bloc
。
值得一提的是,具体做法上 Flutter 和原生 MVP 框架有所不同。
咱们晓得在经典 MVP 模式中,
个别 View 和 Presenter 以接口定义本身行为 (action),
互相持有接口进行调用。
但 Flutter 中不太适宜这么做,
从 Presenter → View 关系上 View 在 Flutter 中对应 Widget,
但在 Flutter 中 Widget 只是用户申明 UI 的配置,
间接管制 Widget 实例并不是好的做法。
而在从 View → Presenter 的关系上,
Widget 能够的确能够间接持有 Presenter,
然而这样又会带来难以数据通信的问题。
这一点不同状态治理框架的解决思路不一样,从实现上他们能够分为两大类:
- 通过 Flutter 树机制 解决,例如 Provider;
- 通过 依赖注入,例如 Get。
1) 通过 Flutter 树机制解决 V → P 的获取
abstract class Element implements BuildContext {
/// 以后 Element 的父节点
Element? _parent;
}
abstract class BuildContext {
/// 查找父节点中的 T 类型的 State
T findAncestorState0fType<T extends State>( );
/// 遍历子元素的 element 对象
void visitChildElements(ElementVisitor visitor);
/// 查找父节点中的 T 类型的 InheritedWidget 例如 MediaQuery 等
T dependOnInheritedWidget0fExactType<T extends InheritedWidget>({Object aspect});
……
}
<center> Element 实现了父类 BuildContext 中操作树结构的办法 </center>
咱们晓得 Flutter 中存在三棵树,Widget、Element 和 RenderObject。
所谓的 Widget 树其实只是咱们形容组件嵌套关系的一种说法,是一种虚构的构造 。
但 Element 和 RenderObject 在运行时理论存在,
能够看到 Element 组件中蕴含了 _parent
属性,寄存其父节点。
而它实现了 BuildContext
接口,蕴含了诸多对于树结构操作的办法,
例如 findAncestorStateOfType
,向上查找父节点;visitChildElements
遍历子节点。
在一开始的例子中,咱们能够通过 context.findAncestorStateOfType
一层一层地向上查找到须要的 Element 对象,
获取 Widget 或者 State 后即可取出须要的变量。
provider 也是借助了这样的机制,实现了 View -> Presenter 的获取。
通过 Provider.of
获取顶层 Provider 组件中的 Present 对象。
显然,所有 Provider 以下的 Widget 节点,
都能够通过本身的 context 拜访到 Provider 中的 Presenter,
很好地解决了跨组件的通信问题。
2) 通过依赖注入的形式解决 V → P
树机制很不错,但依赖于 context,这一点有时很让人抓狂。
咱们晓得 Dart 是一种单线程的模型,
所以不存在多线程下对于对象拜访的竞态问题。
基于此 Get 借助一个全局单例的 Map 存储对象。
通过依赖注入的形式,实现了对 Presenter 层的获取。
这样在任意的类中都能够获取到 Presenter。
这个 Map 对应的 key 是 runtimeType
+ tag
,
其中 tag 是可选参数,而 value 对应 Object
,
也就是说咱们能够存入任何类型的对象,并且在任意地位获取。
解决难以跨组件 (跨页面) 拜访数据的问题
这个问题其实和上一部分的思考根本相似,所以咱们能够总结一下两种计划特点:
Provider
- 依赖树机制,必须基于 context
- 提供了子组件拜访下层的能力
Get
- 全局单例,任意地位能够存取
- 存在类型反复,内存回收问题
解决高层级 setState 引起不必要刷新的问题
最初就是咱们提到的高层级 setState
引起不必要刷新的问题,
Flutter 通过采纳观察者模式解决,其关键在于两步:
- 观察者去订阅被察看的对象;
- 被察看的对象告诉观察者。
零碎也提供了 ValueNotifier
等组件的实现:
/// 申明可能变动的数据
ValueNotifier<int> _statusNotifier = ValueNotifier(0);
ValueListenableBuilder<int>(
// 建设与 _statusNotifier 的绑定关系
valueListenable: _statusNotifier,
builder: (c, data, _) {return Text('$data');
})
/// 数据变动驱动 ValueListenableBuilder 部分刷新
_statusNotifier.value += 1;
理解到最根底的观察者模式后,看看不同框架中提供的组件:
比方 Provider 中提供了 ChangeNotifierProvider
:
class Counter extend ChangeNotifier {
int count = 0;
/// 调用此办法更新所有察看节点
void increment() {
count++;
notifyListeners();}
}
void main() {
runApp(
ChangeNotifierProvider(
/// 返回一个实现 ChangeNotifier 接口的对象
create: (_) => Counter(),
child: const MyApp(),),
);
}
/// 子节点通过 Consumer 获取 Counter 对象
Consumer<Counter>(builder:(_, counter, _) => Text(counter.count.toString())
还是之前计数器的例子,这里 Counter
继承了 ChangeNotifier
通过顶层的 Provider 进行存储。
子节点通过 Consumer 即可获取实例,
调用了 increment
办法之后,只有对应的 Text 组件进行变动。
同样的性能,在 Get 中,
只须要提前调用 Get.put
办法存储 Counter
对象,
为 GetBuilder
组件指定 Counter
作为泛型。
因为 Get 基于单例,所以 GetBuilder
能够间接通过泛型获取到存入的对象,
并在 builder 办法中裸露。这样 Counter
便与组件建设了监听关系,
之后 Counter
的变动,只会驱动以它作为泛型的 GetBuilder
组件更新。
class Counter extends GetxController {
int count = 0;
void increase() {
count++;
update();}
}
/// 提前进行存储
final counter = Get.put(Counter());
/// 间接通过泛型获取存储好的实例
GetBuilder<Counter>(builder: (Counter counter) => Text('${counter.count}') );
实际中的常见问题
在应用这些框架过程中,可能会遇到以下的问题:
Provider 中 context 层级过高
class MyApp extends StatelessWidget {const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(create: (_) => const Count(),
child: MaterialApp(
home: Scaffold(body: Center(child: Text('${Provider.of<Counter>(context).count}')),
),
),
);
}
}
如代码所示,当咱们间接将 Provider 与组件嵌套于同一层级时,
这时代码中的 Provider.of(context)
运行时抛出 ProviderNotFoundException
。
因为此处咱们应用的 context 来自于 MyApp,
但 Provider 的 element 节点位于 MyApp 的下方,
所以 Provider.of(context)
无奈获取到 Provider 节点。
这个问题能够有两种改法,如下方代码所示:
改法 1: 通过嵌套 Builder 组件,应用子节点的 context 拜访:
class MyApp extends StatelessWidget {const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Provider(create: (_) => const Count(),
child: MaterialApp(
home: Scaffold(
body: Center(child: Builder(builder: (builderContext) {return Text('${Provider.of<Counter>(builderContext).count}');
}),
),
),
),
);
}
}
改法 2: 将 Provider 提至顶层:
void main() {
runApp(
Provider(create: (_) => Counter(),
child: const MyApp(),),
);
}
class MyApp extends StatelessWidget {const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(body: Center(child: Text('${Provider.of<Counter>(context).count}')),
),
);
}
}
Get 因为全局单例带来的问题
正如后面提到 Get 通过全局单例,默认以 runtimeType
为 key 进行对象的存储,
局部场景可能获取到的对象不合乎预期,例如商品详情页之间跳转。
因为不同的详情页实例对应的是同一 Class,即 runtimeType
雷同。
如果不增加 tag 参数,在某个页面调用 Get.find
会获取到其它页面曾经存储过的对象。
同时 Get 中肯定要留神思考到对象的回收,不然很有可能引起内存透露。
要么手动在页面 dispose
的时候做 delete
操作,
要么齐全应用 Get 中提供的组件,例如 GetBuilder
,
它会在 dispose
中开释。
GetBuilder
中在 dispose
阶段进行回收:
@override
void dispose() {super.dispose();
widget.dispose?.call(this);
if (_isCreator! || widget.assignId) {if (widget.autoRemove && GetInstance().isRegistered<T>(tag: widget.tag)) {GetInstance().delete<T>(tag: widget.tag);
}
}
_remove?.call();
controller = null;
_isCreator = null;
_remove = null;
_filter = null;
}
Get 与 Provider 优缺点总结
通过本文,我向大家介绍了状态治理的必要性、它解决了 Flutter 开发中的哪些问题以及是如何解决的,
与此同时,我也为大家总结了在实践中常见的问题等,看到这里你可能还会有些纳闷,到底是否须要应用状态治理?
在我看来,框架是为了解决问题而存在。所以这取决于你是否也在经验一开始提出的那些问题。
如果有,那么你能够尝试应用状态治理解决;如果没有,则没必要适度设计,为了应用而应用。
其次,如果应用状态治理,那么 Get 和 Provider 哪个更好?
这两个框架各有优缺点,我认为如果你或者你的团队刚接触 Flutter,
应用 Provider 能帮忙你们更快了解 Flutter 的外围机制。
而如果曾经对 Flutter 的原理有理解,Get 丰盛的性能和简洁的 API,
则能帮忙你很好地进步开发效率。
感激社区成员 Alex、Luke、Lynn、Ming 对本文的奉献。