一、问题概述
之前在使用 FutureBuilder 的过程中发现了一个问题,就是每次刷新界面的时候,FutrueBuilder 中的 future 函数都会执行,虽然不会造成什么严重问题(甚至有的时候需求就是这样的),但是在某些情况下,这个问题是一定要解决的(后面会说明)。
github 上有一个相关的 issue: FutureBuilder fire 。
我在 Medium 上看到了一篇相关的文章来解释这个问题,这里分享给大家。
二、问题复现
要复现这个问题很简单,只要页面中有 FutureBuilder 组件,那么每次调用 setState 都会导致 FutureBuilder 刷新一次,future 函数执行一次。
假设我们的代码如下:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen()
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
bool _switchValue;
@override
void initState() {
super.initState();
this._switchValue = false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Switch(
value: this._switchValue,
onChanged: (newValue) {
setState(() {
this._switchValue = newValue;
});
},
),
FutureBuilder(
future: this._fetchData(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return Center(
child: CircularProgressIndicator()
);
default:
return Center(
child: Text(snapshot.data)
);
}
}
),
],
),
);
}
_fetchData() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
}
}
- Switch 组件状态会触发 setState()
- FutrueBuilder 通过 _fetchData() 来模拟从服务器获取数据
运行上面的代码,会出现下面的情况。
可以发现,每次 switch 状态的改变,都会导致 FutrueBuilder 刷新一次,但是在上面的代码里,switch 和 FutureBuilder 其实是没有任何联系的。由于任何 setState 操作都会导致 FutureBuilder 刷新一次,那么可能就会导致以下问题:
- 当界面已经不可见时依然有代码在执行(消耗性能与流量等)
- 热重载不正确
- 在 Inherited 组件里面更新数值将导致 Navigator 状态丢失
- etc...
三、原因分析
FutureBuilder 其实是一个 StatefulWidget 类型的组件。
class FutureBuilder<T> extends StatefulWidget {
/// Creates a widget that builds itself based on the latest snapshot of
/// interaction with a [Future].
///
/// The [builder] must not be null.
const FutureBuilder({
Key key,
this.future,
this.initialData,
@required this.builder,
}) : assert(builder != null),
super(key: key);
...
}
StatefulWidgets 组件将会持有一个生命周期很长的 State 对象,这个 State 对象有很多方法和生命周期有关。
- initState : 在对象创建之后调用一次。
- build 每次当我们刷新展示组件时,这个方法都会被调用。
- didUpdateWidget 当一个老组件被回收一个新组件重建,并且这个新 Widget 和当前 State 对象绑定到一起的时候,didUpdateWidget 就会被回调。简单的说就是,当一个组件关联的 State 对象改变时,didUpdateWidget 就会执行,这里可以进行一些 rebuild 之前需要进行的操作。
在 FutureBuilder 里面,didUpdateWidget方法重写如下:
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
在这里会进行判断,当新的 widget 和 老的 widget 的 future 不是同一个对象实例的时候,就会重复执行一遍: _unsubscribe() 和 _subscribe() ,也就是 FutureBuilder 的生命周期重新走了一遍。
因此现在的问题就是,每次界面刷新的时候,FutureBuilder 老状态组件的 future 实例都和新状态组件的 futrue 实例不是同一个,那么我们要做的就是,让 FutureBuilder 新老组件获取相同的 future 实例即可。
四、解决方案
在一些函数式语言里面,当一个函数确定之后,只要输入相同,那么这个函数一定会输出相同的结果,因此我们可以把这个结果存储起来,当这个函数再次被调用的时候,我们直接返回上一次运行的结果即可。
放到我们这里就是,我们需要把 futrue 对象实例存储起来,每次都获取这个相同的实例,就可以解决上面的问题。
dart 里面正好提供了一个类 AsyncMemoizer 可以满足我们的需求。
总结一下就是,这个类可以保证函数只执行一次,并且把结果存储起来,当这个函数多次被调用时,就把这个结果返回。
要使用这个类,先他添加 async 库依赖(注意依赖版本不要冲突):
async: ^2.3.0
我们在代码里面初始化 AsnycMemorizer 对象实例:
final AsyncMemoizer _memoizer = AsyncMemoizer();
最后把之前定义的 future 函数结合 memorizer 使用:
_fetchData() {
return this._memoizer.runOnce(() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
});
最终效果如下:
这里可能有人有疑问,就是这样实现之后,每次刷新都不会再刷新 FutureBuilder 了,那么如果我想控制到底要不要刷新,我该怎么做?我觉得可以这样,加入一个变量,根据这个变量来判断是否返回相同实例,从而达到是否刷新的效果。
_fetchData(bool flag) async{
if(flag){
return this._memoizer.runOnce(() async {
await Future.delayed(Duration(seconds: 2));
return 'REMOTE DATA';
});
}else{
await Future.delayed(Duration(seconds: 5));
return 'REMOTE DATA';
}
}
最后
欢迎关注「Flutter 编程开发」微信公众号 。