Flutter是一个响应式UI框架,当需要展示界面的时候,框架通过build方法生成一帧的画面,当画面频繁变化时,flutter会重复调用build方法来生成每一帧。所以不合适的函数调用或者当build和一些Widget(例如:FutureBuilder,StreamBuilder)合用时,可能会产生一些副作用。
以下是会产生副作用的示例:
import 'random_color.dart' as color;
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: FutureBuilder(
future: color.randomColors(),
builder: (context, snapshot) {
if (snapshot.hasData) {
final colors = snapshot.data;
return Column(
children: <Widget>[
Expanded(flex: 4, child: Container(color: colors[0])),
Expanded(flex: 3, child: Container(color: colors[1])),
Expanded(flex: 2, child: Container(color: colors[2])),
Expanded(flex: 2, child: Container(color: colors[3])),
],
);
} else {
return Container();
}
}),
);
}
}
其中randomColor实现如下:
Future<List<Color>> randomColors() async {
await loadData();
return randomColorsSync();
}
var functionCallCount = 0;
List<Color> randomColorsSync() {
assert(data.isNotEmpty);
final code = data[Random().nextInt(data.length)];
functionCallCount++;
print("Function call count: $functionCallCount");
return [
code.substring(0, 6),
code.substring(6, 12),
code.substring(12, 18),
code.substring(18, 24),
].map((e) => hexColor("#$e")).toList();
}
为了便于分析,我在randomColorSync函数内部添加了统计函数调用计数的代码,用于将调用次数显示在控制台上。
除此之外,我们创建了一个辅助Widget:HomeWrapWidget来手动触发Widget重建:
class HomeWrapWidget extends StatefulWidget {
@override
_HomeWrapWidgetState createState() => _HomeWrapWidgetState();
}
class _HomeWrapWidgetState extends State<HomeWrapWidget> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: MyHomePage(),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _rebuild(context),
child: Icon(Icons.tag_faces),
),
),
);
}
void _rebuild(BuildContext context) {
setState(() {});
}
}
最终的界面如下:
这是启动后的控制台日志:
I/flutter ( 4259): Function call count: 1
当我们重复点击悬浮按钮时,这个时候的控制台日志为:
I/flutter ( 4259): Function call count: 2
I/flutter ( 4259): Function call count: 3
I/flutter ( 4259): Function call count: 4
I/flutter ( 4259): Function call count: 5
I/flutter ( 4259): Function call count: 6
···
每次按下按钮后,界面的配色都发生了变化,重新获取了新的配色方案。
但是这并不是我们想要的效果,使用FutureBuilder的初衷时为了方便将Future数据映射为UI,但是当我们更新界面其他部分时,却导致FutureBuilder再次获取了一遍数据,这种不符合直觉的结果在某些时候可能会产生一些错误。在示例部分我们使用了随机配色配合FutureBuilder构建UI,所以当错误发生的时候,我们很明显可以观察到,但是如果将randomColor函数替换为一个http请求函数,当你请求同一个网址,返回相同的资源的时候,错误发生后,仅从界面是观察不到相应的变化的。
当我们更新界面其他部分的时候,却导致了对返回Future数据的资源的反复请求,尤其在发生路由页面动画的时候,会触发整个页面的Widget树重建,按照流畅应用60FPS的要求计算,对于http请求每秒会发送60次,算上动画时长,多设备多用户,对用户流量,对服务器都是极大的负担,同时每次请求返回不同的Future对象,还会导致FutureBuilder重复调用builder方法,这就是我在标题里提到的副作用。
对于这些副作用,解决方案有以下3种:
-
在initState方法里获取Future对象并缓存
由于
initState方法在整个StatefulWidget的生命周期中只会调用一次,所以对于Future、Stream,可以在该方法中将需要用到的返回结果缓存下来,供后续使用。如果调用的方法需要传递BuildContext对象,也可以在didChangeDependencies方法中缓存调用结果。didChangeDependencies方法会在Element依赖发生改变的时候被调用。code:
class _MyHomePage2State extends State<MyHomePage2> { Future<List<Color>> colorsFuture; @override void initState() { colorsFuture = color.randomColors(); super.initState(); } @override Widget build(BuildContext context) { return Container( color: Colors.white, child: FutureBuilder( future: colorsFuture, builder: (context, snapshot) { if (snapshot.hasData) { final colors = snapshot.data; return Column( children: <Widget>[ Expanded(flex: 4, child: Container(color: colors[0])), Expanded(flex: 3, child: Container(color: colors[1])), Expanded(flex: 2, child: Container(color: colors[2])), Expanded(flex: 2, child: Container(color: colors[3])), ], ); } else { return Container(); } }), ); } } -
使用AsyncMemoizer
正如它的类名,
AsyncMemoizer实质上就是一个内存缓存,runOnce方法保证只运行一次函数,并在之后使用缓存的异步结果。code:
import 'package:async/async.dart' show AsyncMemoizer; class MyHomePage3 extends StatefulWidget { @override _MyHomePage3State createState() => _MyHomePage3State(); } class _MyHomePage3State extends State<MyHomePage3> { final _memoizer = AsyncMemoizer<List<Color>>(); @override Widget build(BuildContext context) { return Container( color: Colors.white, child: FutureBuilder( future: _memoizer.runOnce(color.randomColors), builder: (context, snapshot) { if (snapshot.hasData) { final colors = snapshot.data; return Column( children: <Widget>[ Expanded(flex: 4, child: Container(color: colors[0])), Expanded(flex: 3, child: Container(color: colors[1])), Expanded(flex: 2, child: Container(color: colors[2])), Expanded(flex: 2, child: Container(color: colors[3])), ], ); } else { return Container(); } }), ); } } -
由外部管理Future
这次不在Widget内部缓存Future结果,而是由外部管理,保证只获取一次,返回相同的Future对象
code:
var myColors = color.randomColors(); class MyHomePage4 extends StatelessWidget { @override Widget build(BuildContext context) { return FutureBuilder( future: myColors, builder: (context, snapshot) { if (snapshot.hasData) { return ColorListWidget(colors: snapshot.data); } else { return Container(); } }); } } class ColorListWidget extends StatelessWidget { final List<Color> colors; const ColorListWidget({Key key, @required this.colors}) : super(key: key); @override Widget build(BuildContext context) { return Column( children: <Widget>[ buildColorTile(flex: 4, color: colors[0]), buildColorTile(flex: 3, color: colors[1]), buildColorTile(flex: 2, color: colors[2]), buildColorTile(flex: 2, color: colors[3]), ], ); } Widget buildColorTile({int flex = 1, Color color}) { return Expanded( flex: flex, child: Container( color: color, child: Stack( children: <Widget>[ Positioned( left: 0, bottom: 0, child: Text( "#${color.value.toRadixString(16).padLeft(8, '0').substring(2).toUpperCase()}", style: TextStyle( color: ThemeData.estimateBrightnessForColor(color) == Brightness.light ? Colors.black.withOpacity(0.9) : Colors.white.withOpacity(0.9), ), ), ), ], ), ), ); } }
最终效果: