Flutter FutureBuilder 优雅构建异步UI
际开发中, 一般在展示列表内容之前需要先展示一个 loading
表示正在加载, 当加载成功后展示列表内容, 加载失败展示失败的界面
所以, 这样一个需求就涉及到了三种情况:
- 加载中
- 加载成功展示列表
- 加载失败展示错误
我们知道 Flutter UI
是 声明式UI
不同UI的切换时通过 setState
来重新构建的. 那么上面的三种情况UI 我们需要通过 if else
来判断到底展示那种界面.
例如下面的伪代码:
@override
Widget build(BuildContext context) {
if(loading) { // 正在加载
return Text("Loading...");
} else if(isError) { // 加载出错
return Text("Error...");
} else { // 展示列表内容
return ListView(...)
}
}
复制代码
这种方式虽然也能实现上面的需求, 但是不利于代码的维护, 需要维护很多变量, 很不优雅.
FutureBuilder
FutureBuilder
的用法很简单, 主要涉及两个参数:
future
指定异步任务, 交给FutureBuilder
管理builder
根据异步任务的状态来构建不同的Widget
, 类似上面的if/else
FutureBuilder
中的异步任务状态有:
状态 | 描述 |
---|---|
none | 没有连接到任何异步任务 |
waiting | 已连接到异步任务等待被交互 |
active | 已连接到一个已激活的异步任务 |
done | 已连接到一个已结束的异步任务 |
我们可以使用 FutureBuilder
改造上面的案例, 代码如下所示:
FutureBuilder<int>(
future: _loadList(),
builder: (context, snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
case ConnectionState.active:
// 显示正在加载
return createLoadingWidget();
case ConnectionState.done:
// 提示错误信息
if (snapshot.hasError) {
return createErrorWidget(snapshot.error.toString());
}
// 展示列表内容
return ListView.separated(
itemCount: snapshot.data,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(index.toString()));
},
separatorBuilder: (BuildContext context, int index) {
return divider;
},
);
default:
return Text("unknown state");
)
复制代码
需要注意的是, 上面的代码界面每次被重建的时候都会执行 loadList 操作.
但是有的时候并不是界面发生变化的时候都需要去重新执行 future
, 例如界面一个 Tab + ListView(文章分类+文章列表)
, 文章分类是需要先加载, 那么文章分类的异步任务就是 future
, 加载成功分类后, 才能去加载文章列表, 列表加载成功界面会重新构建, 这个时候是不应该再次加载文章分类的(future)
这个时候需要在把 future
变量作为成员变量, 在 initState
中初始化, 然后再传递给 future
参数, 如:
Future _future;
@override
void initState() {
_future = _loadList();
super.initState();
}
FutureBuilder<int>(
future: _future,
...
)
复制代码
运行效果如下图所示:
FutureBuilder 源码分析
FutureBuilder
继承了 StatefulWidget
, 所以主要代码都集中在 State
中
class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
Object _activeCallbackIdentity;
AsyncSnapshot<T> _snapshot;
@override
void initState() {
super.initState();
// 初始化异步快照, 初始状态为 none
_snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);
// 关联异步任务
_subscribe();
}
// 页面发生变化判断老的widget的 future 和新widget future 是否是同一个对象
// 如果是同一个对象则不会执行异步任务, 否则会重新执行异步任务
@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.future != widget.future) {
if (_activeCallbackIdentity != null) {
_unsubscribe();
_snapshot = _snapshot.inState(ConnectionState.none);
}
_subscribe();
}
}
// 执行外部传入的 builder 回调
// widget 就是 State 对应的 FutureBuilder(StatefulWidget)
@override
Widget build(BuildContext context) => widget.builder(context, _snapshot);
@override
void dispose() {
_unsubscribe();
super.dispose();
}
void _subscribe() {
if (widget.future != null) {
final Object callbackIdentity = Object();
_activeCallbackIdentity = callbackIdentity;
// 开始执行异步任务
widget.future.then<void>((T data) {
if (_activeCallbackIdentity == callbackIdentity) {
// 刷新界面
setState(() {
// 组件异步快照数据
_snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
});
}
}, onError: (Object error) {
// 执行异步任务发生异常
if (_activeCallbackIdentity == callbackIdentity) {
setState(() {
_snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error);
});
}
});
// 将异步任务状态设置为 waiting
_snapshot = _snapshot.inState(ConnectionState.waiting);
}
}
void _unsubscribe() {
_activeCallbackIdentity = null;
}
复制代码
StreamBuilder
除了 FutureBuilder
可以优雅构建异步UI, StreamBuilder
也可以实现, 但是一般的异步任务 UI 展示并不是一个 Stream
流的形式, 更像是一次性的逻辑处理, 只要成功后, 一般不需要更新, 所以使用 FutureBuilder
就完全够了. 实际开发中根据情况来选择. StreamBuilder
的功能更加强大, 后期如果往 stream
中发送数据 UI 界面也跟着发生变化 如:
StreamBuilder<int>(
// 这个是stream 而不是 future
stream: _streamController.stream,
initialData: _counter,
builder: (BuildContext context, AsyncSnapshot<int> snapshot){
// 接收到 controller 发送给 stream 的数据
return Text('${snapshot.data}');
}
),
)
复制代码
我们可以通过_streamController
发送数据, 然后会自动调用 StreamBuilder builder
回调, 从而刷新 Widget
_streamController.sink.add(++_counter);
复制代码
当然也可以不通过 StreamController
来提供 stream
, 也可以创建一个函数返回 stream。
作者:Chiclaim
来源:掘金