一般程序员都会了解,类似于 IO、网络请求等都应该是异步的。
在Dart中,我们使用 Future 来管理,这样就不用担心线程或者死锁的问题。
那么当 Flutter 涉及到 Future 的时候,widget 该如何去构建呢?
在网络请求 开始前、请求中、请求完成或失败,我们应该如何去管理我们的UI?
为此,Flutter 推出 FutureBuilder。
什么是FutureBuilder
先看文档:
Widget that builds itself based on the latest snapshot of interaction with a Future.
翻译过来说就是 FutureBuilder 是基于 Future 快照来构建自身的一个组件。
快照是啥玩意?个人理解就是这个 Future 目前的信息。
这个信息里面包括:目前的状态、所携带的数据等等。
如何使用
先看看 FutureBuilder 是个啥, 点开源码:
class FutureBuilder<T> extends StatefulWidget {
const FutureBuilder({
Key key,
this.future,
this.initialData,
@required this.builder,
}) : assert(builder != null),
super(key: key);
}
final AsyncWidgetBuilder builder;
复制代码
看出来是个有状态的小部件,找到 State 的 build 方法:
Widget build(BuildContext context) => widget.builder(context, _snapshot);
build 方法直接返回了一个 widget 的 builder。
那我们继续,打开官网看官方Demo怎么写的:
FutureBuilder<String>(
future: _calculation, // a previously-obtained Future or null
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return Text('Press button to start.');
case ConnectionState.active:
case ConnectionState.waiting:
return Text('Awaiting result...');
case ConnectionState.done:
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
return Text('Result: ${snapshot.data}');
}
return null; // unreachable
},
)
复制代码
可以看到 FutureBuilder 定义了一个泛型,这个泛型是用来获取快照中数据时用的。
FlutureBuilder 有两个参数:
future:这个参数需要一个 Future 对象,类似于 网络请求、IO
builder:这个参数需返回一个 widget,我们可以看到 demo 中根据现在快照不同的连接状态返回不同的 widget。
我们再来看一下 snapshot.connectionState
都有哪些值:
ConnectionState | 当前没有连接到任何的异步任务 |
---|---|
ConnectionState.none | 当前没有连接到任何的异步任务 |
ConnectionState.waiting | 连接到异步任务并等待进行交互 |
ConnectionState.active | 连接到异步任务并开始交互 |
ConnectionState.done | 异步任务中止 |
现在了解了之后我们就可以有想法了。
我们在打开一个页面的时候肯定会有网络请求,这个时候要显示 loading 之类的,我们就可以利用当前快照的状态来返回不同的 widget,比如这样:
首先看build代码:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FutureBuilderPage'),
),
body: FutureBuilder(
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.active:
case ConnectionState.waiting:
print('waiting');
return Center(child: CupertinoActivityIndicator());
case ConnectionState.done:
print('done');
if (snapshot.hasError) {
return Center(
child: Text('网络请求出错'),
);
}
return generateListView();
}
return null;
},
future: _future,
),
);
}
复制代码
Scaffold 的 body 直接返回一个 FutureBuilder,根据不同状态来返回了不同的 widget。
**这里需要注意的一点是:**我们知道 StatefulWidget会长时间维护一个 State,当有变动的时候会调用 didUpdateWidget
方法,就要重新build了。所以FutureBuilder
的官方文档上有这么一段文字:
The future must have been obtained earlier, e.g. during State.initState,
State.didUpdateConfig
, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.buildmethod call when constructing the FutureBuilder. If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.A general guideline is to assume that every
build
method could get called every frame, and to treat omitted calls as an optimization.
大致意思就是说 future
这个参数建议在 initState() 里初始化,不要在 build 方法里初始化,这样的话会一直 rebuild。
为什么呢,我们查看 didUpdateWidget
源码:
@override
void didUpdateWidget(FutureBuilder oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
复制代码
可以看出来这里是判断了 future 这个字段,所以我们一定不要在 build 方法里初始化 future 参数!
所以,我们在 initState()方法里初始化:
Future _future;
Dio _dio;
int date = 20190523;
List _newsData = [];
@override
void initState() {
super.initState();
_dio = Dio();
_future = getNewsList();
}
// 获取知乎每天的新闻,数据获取成功后 setState来刷新数据
Future getNewsList() async {
var response =
await _dio.get('https://news-at.zhihu.com/api/4/news/before/$date');
setState(() {
_newsData.addAll(ZhiHuNews.fromJson(response.data)._stories);
});
}
复制代码
generateListView 方法就不放了,就是基本的 ListView.builder()。
这样我们就完成了上图的效果,在网络请求的时候加载小菊花,请求成功加载出 ListView.
小结
可以看得出来 FutureBuilder 确实是非常方便,而且我们可以自己封装几个控件,后面用的时候就会更加完美。
如想获取源码请移步本人Github:github.com/wanglu1209/…