Flutter FutureBuilder 优雅构建异步UI

2019-08-2708:15:16APP与小程序开发Comments5,029 views字数 3638阅读模式

际开发中, 一般在展示列表内容之前需要先展示一个 loading 表示正在加载, 当加载成功后展示列表内容, 加载失败展示失败的界面文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

所以, 这样一个需求就涉及到了三种情况:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

  • 加载中
  • 加载成功展示列表
  • 加载失败展示错误

 文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

我们知道 Flutter UI声明式UI文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

不同UI的切换时通过 setState 来重新构建的. 那么上面的三种情况UI 我们需要通过 if else 来判断到底展示那种界面.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

例如下面的伪代码:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

@override
Widget build(BuildContext context) {
    if(loading) { // 正在加载
      return Text("Loading...");
    } else if(isError) { // 加载出错
      return Text("Error...");
    } else {   // 展示列表内容
      return ListView(...)
    }
}
复制代码

这种方式虽然也能实现上面的需求, 但是不利于代码的维护, 需要维护很多变量, 很不优雅.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

FutureBuilder

FutureBuilder 的用法很简单, 主要涉及两个参数:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

  • future 指定异步任务, 交给 FutureBuilder 管理
  • builder 根据异步任务的状态来构建不同的 Widget, 类似上面的 if/else

FutureBuilder 中的异步任务状态有:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

状态描述
none没有连接到任何异步任务
waiting已连接到异步任务等待被交互
active已连接到一个已激活的异步任务
done已连接到一个已结束的异步任务

我们可以使用 FutureBuilder 改造上面的案例, 代码如下所示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

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 操作.文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

但是有的时候并不是界面发生变化的时候都需要去重新执行 future, 例如界面一个 Tab + ListView(文章分类+文章列表), 文章分类是需要先加载, 那么文章分类的异步任务就是 future, 加载成功分类后, 才能去加载文章列表, 列表加载成功界面会重新构建, 这个时候是不应该再次加载文章分类的(future)文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

这个时候需要在把 future 变量作为成员变量, 在 initState 中初始化, 然后再传递给 future 参数, 如:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

Future _future;

@override
void initState() {
    _future = _loadList();
    super.initState();
}

FutureBuilder<int>(
    future: _future,
    ...
)
复制代码

运行效果如下图所示:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

Flutter FutureBuilder 优雅构建异步UI

FutureBuilder 源码分析

FutureBuilder 继承了 StatefulWidget, 所以主要代码都集中在 State文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

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 界面也跟着发生变化 如:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

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文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

_streamController.sink.add(++_counter);
复制代码

当然也可以不通过 StreamController 来提供 stream, 也可以创建一个函数返回 stream。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

作者:Chiclaim
来源:掘金文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/xcx/15837.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/xcx/15837.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定