我们经常有这样的一个开发场景:一个页面进入之后先进行网络请求,此时显示一个圆圈(等待动画),等网络数据返回时显示一个展示网络数据的布局。例如下图:
我们通常的做法是
if(data==null){
return CircularProgressIndicator();
}else{
return ListView(...);
}
大致就是数据返回之前我们加载一个组件,等数据返回值后,我们重绘页面返回另一个组件。
在flutter中,有一个新的实现方式,那就是我们即将要介绍的futureBuilder.
Widget that builds itself based on the latest snapshot of interaction with a Future.
官方意思是一个基于与Future交互的最新快照构建自己的小部件。
先看一下它的构造方法:
const FutureBuilder({
Key key,
this.future, //获取数据的方法
this.initialData, //初始的默认数据
@required this.builder
}) : assert(builder != null),
super(key: key);
主要看一下builder,这个是我们主要关心的,它是我们构建组件的策略。
接收两个参数:BuildContext context, AsyncSnapshot snapshot.
context就不解释了,snapshot就是_calculation在时间轴上执行过程的状态快照。
//FutureBuilder控件
new FutureBuilder<String>(
future: _calculation, // 用户定义的需要异步执行的代码,类型为Future或者null的变量或函数
builder: (BuildContext context, AsyncSnapshot<String> snapshot) { //snapshot就是_calculation在时间轴上执行过程的状态快照
switch (snapshot.connectionState) {
case ConnectionState.none: return new Text('Press button to start'); //如果_calculation未执行则提示:请点击开始
case ConnectionState.waiting: return new Text('Awaiting result...'); //如果_calculation正在执行则提示:加载中
default: //如果_calculation执行完毕
if (snapshot.hasError) //若_calculation执行出现异常
return new Text('Error: ${snapshot.error}');
else //若_calculation执行正常完成
return new Text('Result: ${snapshot.data}');
}
},
)
FutureBuilder通过子属性future
获取用户需要异步处理的代码,用builder
回调函数暴露出异步执行过程中的快照。我们通过builder的参数snapshot
暴露的快照属性,定义好对应状态下的处理代码,即可实现异步执行时的交互逻辑。
看起来似乎有点绕口,我们看看下面这段代码:
/*
* Created by 李卓原 on 2018/9/30.
* email: [email protected]
* 关于状态改变引起的不必要的页面刷新:https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
*/
import 'dart:async';
import 'package:async/async.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app/utils/HttpUtil.dart';
class FutureBuilderPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => FutureBuilderState();
}
class FutureBuilderState extends State<FutureBuilderPage> {
String title = 'FutureBuilder使用';
Future _gerData() async {
var response = HttpUtil()
.get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
return response;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
title = title + '.';
});
},
child: Icon(Icons.title),
),
body: FutureBuilder(
builder: _buildFuture,
future: _gerData(), // 用户定义的需要异步执行的代码,类型为Future或者null的变量或函数
),
);
}
///snapshot就是_calculation在时间轴上执行过程的状态快照
Widget _buildFuture(BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('还没有开始网络请求');
return Text('还没有开始网络请求');
case ConnectionState.active:
print('active');
return Text('ConnectionState.active');
case ConnectionState.waiting:
print('waiting');
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
print('done');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return _createListView(context, snapshot);
default:
return null;
}
}
Widget _createListView(BuildContext context, AsyncSnapshot snapshot) {
List movies = snapshot.data['subjects'];
return ListView.builder(
itemBuilder: (context, index) => _itemBuilder(context, index, movies),
itemCount: movies.length * 2,
);
}
Widget _itemBuilder(BuildContext context, int index, movies) {
if (index.isOdd) {
return Divider();
}
index = index ~/ 2;
return ListTile(
title: Text(movies[index]['title']),
leading: Text(movies[index]['year']),
trailing: Text(movies[index]['original_title']),
);
}
}
在build方法中,我们返回了一个Scaffold,主要的代码在body中,包裹了一个FutureBuilder,
我们在它的builder方法中,对不同状态返回了不同的控件。
snapshot.connectionState
就是异步函数_gerData
的执行状态,用户通过定义在ConnectionState.none
和ConnectionState.waiting
状态下,输出一个Text和居中·(Center)·显示并且内置文字CircularProgressIndicator的组件,其意义即:当异步函数_gerData
未执行时,屏幕正中央显示文字:还没有开始网络请求
。和正在执行时,显示一个刷新状态的控件。
当_gerData
执行完毕后,snapshot.connectionState
的值即变为ConnectionState.done
,此时即可输出根据HTTP请求获取到的数据生成对应的ListItem。由于ConnectionState.done
是除了ConnectionState.none
和ConnectionState.waiting
以外的唯一值,所以代码中在switch下用default也可(ConnectionState.active
好像在整个过程中没有调用)。
由于通过FutureBuilder
内的builder()
函数即可操控控件的状态和重绘,我们不必通过自己写异步状态的判断和多次使用setState()
实现页面上加载中和加载完成显示效果的切换,因为FutureBuilder
内部自带了执行setState()
的方法。
现在一个FutureBuilder的构建就算完成了。
如果只是写一个FutureBuilder,我们就不需要floatingActionButton里的一系列东西,所以这时候就到它的出场了。
代码中的意思,每次点击它,就在我们标题后面加一个“.” , 看一下效果
确实是改变了标题,但是整个页面也随着setState而进行了不必要的重绘,这就是我们本篇的重点了。
即使AppBar和FutureBuilder没有任何关联,每次我们改变它的值(通过调用setState), FutureBuilder都会再次经历整个生命周期!它重新取代future,导致不必要的流量,并再次显示负载,导致糟糕的用户体验。
这个问题以各种方式表现出来。在某些情况下,它甚至不像上面的例子那么明显。例如:
但是这一切的原因是什么?我们如何解决它?
注意:在本节中,我将详细介绍FutureBuilder的工作原理。如果您对此不感兴趣,可以跳到解决方案。
如果我们仔细看看代码FutureBuilder,我们发现它是一个StatefulWidget。我们知道,StatefulWidgets维护一个长期存在的State对象。这种状态有一些管理其生命周期的方法,就像方法initState
,build
和didUpdateWidget
。
initState
在第一次创建状态对象时只调用一次,并且build每次我们需要构建要显示的窗口小部件时调用它,但是那是什么didUpdateWidget
呢?只要附加到此State对象的窗口小部件发生更改,就会调用此方法。
当使用新输入重建窗口小部件时,将放置旧窗口小部件,并创建新窗口小部件并将其分配给State对象,并didUpdateWidget在重建之前调用它以执行我们想要执行的任何操作。
在FutureBuilder
这种情况下,这个方法看起来像这样:
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
它基本上是说:如果在重建时,新窗口小部件具有与旧窗口小部件不同的Future实例,则重复所有内容:取消订阅,并再次订阅。
但我们不是提供相同的Future吗?我们称之为同一个功能!好吧,Future的情况不一样了。我们的功能正在完成同样的工作,但随后又回归了一个与旧的不同的新Future。
因此,我们想要做的是在第一次调用时存储或缓存函数的输出,然后在再次调用函数时提供相同的输出。此过程称为记忆(memoization
)。
简单来说,Memoization
缓存函数的返回值,并在再次调用该函数时重用它。Memoization
主要用于函数式语言,其中函数是确定性的(它们总是为相同的输入返回相同的输出),但我们可以在这里使用简单的memoization
来解决我们的问题,以确保FutureBuilder
始终接收相同的未来实例。
为此,我们将使用Dart
的AsyncMemoizer
。这个记忆器完全符合我们的要求!它需要一个异步函数,在第一次调用它时调用它,并缓存其结果。对于该函数的所有后续调用,memoizer
返回相同的先前计算的未来。
因此,为了解决我们的问题,我们首先在我们的小部件中创建一个AsyncMemoizer实例:
final AsyncMemoizer _memoizer = AsyncMemoizer();
注意:你不应该在StatelessWidget中实例化memoizer,因为Flutter在每次重建时都会处理StatelessWidgets,这基本上可以达到目的。您应该在StatefulWidget中实例化它,或者在它可以持久化的地方实例化它。
之后,我们将修改_fetchData函数以使用该memoizer:
_gerData() {
return _memoizer.runOnce(() async {
return await HttpUtil()
.get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
});
}
我们用AsyncMemoizer.runOnce包装我们的函数,它完全听起来像它的声音;它只运行一次该函数,并在再次调用时返回缓存的Future。
就是这样!我们的FutureBuilder现在只是第一次触发:
现在,我们其他地方进行setState也不会导致FutureBuilder的重绘了。
为了解决这个问题,我们使用Dart的AsyncMemoizer每次都传递相同的Future实例。
问题是每次发布重建时都会调用FutureBuilder
状态的didUpdateWidget
。此函数检查旧的future
对象是否与新的对象不同,如果是,则重新启动FutureBuilder
。为了解决这个问题,我们可以在构建函数之外的某个地方调用Future
。例如,在initState
中,将其保存在成员变量中,并将此变量传递给FutureBuilder
。
比如:
var _futureBuilderFuture;
...
@override
void initState() {
///用_futureBuilderFuture来保存_gerData()的结果,以避免不必要的ui重绘
_futureBuilderFuture = _gerData();
}
...
FutureBuilder(
future: _futureBuilderFuture ,
....
这里使用_futureBuilderFuture来保存_gerData()的结果,这样我们传递给FutureBuilder的是一个成员变量,而不是一个方法就不会多次调用了。
看一下完整代码:
/*
* Created by 李卓原 on 2018/9/30.
* email: [email protected]
* 关于状态改变引起的不必要的页面刷新:https://github.com/flutter/flutter/issues/11426#issuecomment-414047398
*/
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_app/utils/HttpUtil.dart';
class FutureBuilderPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => FutureBuilderState();
}
class FutureBuilderState extends State<FutureBuilderPage> {
String title = 'FutureBuilder使用';
var _futureBuilderFuture;
Future _gerData() async {
var response = HttpUtil()
.get('http://api.douban.com/v2/movie/top250', data: {'count': 15});
return response;
}
@override
void initState() {
// TODO: implement initState
super.initState();
///用_futureBuilderFuture来保存_gerData()的结果,以避免不必要的ui重绘
_futureBuilderFuture = _gerData();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
title = title + '.';
});
},
child: Icon(Icons.title),
),
body: RefreshIndicator(
onRefresh: _gerData,
child: FutureBuilder(
builder: _buildFuture,
future:
_futureBuilderFuture, // 用户定义的需要异步执行的代码,类型为Future或者null的变量或函数
),
),
);
}
///snapshot就是_calculation在时间轴上执行过程的状态快照
Widget _buildFuture(BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
print('还没有开始网络请求');
return Text('还没有开始网络请求');
case ConnectionState.active:
print('active');
return Text('ConnectionState.active');
case ConnectionState.waiting:
print('waiting');
return Center(
child: CircularProgressIndicator(),
);
case ConnectionState.done:
print('done');
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return _createListView(context, snapshot);
default:
return Text('还没有开始网络请求');
}
}
Widget _createListView(BuildContext context, AsyncSnapshot snapshot) {
List movies = snapshot.data['subjects'];
return ListView.builder(
itemBuilder: (context, index) => _itemBuilder(context, index, movies),
itemCount: movies.length * 2,
);
}
Widget _itemBuilder(BuildContext context, int index, movies) {
if (index.isOdd) {
return Divider();
}
index = index ~/ 2;
return ListTile(
title: Text(movies[index]['title']),
leading: Text(movies[index]['year']),
trailing: Text(movies[index]['original_title']),
);
}
}
本文代码地址
网络请求类HttpUtil地址