(十)Flutter FutureBuilder 优雅构建异步UI

3,467 阅读4分钟

前言

在实际开发中, 一般在展示列表内容之前需要先展示一个 loading 表示正在加载, 当加载成功后展示列表内容, 加载失败展示失败的界面

所以, 这样一个需求就涉及到了三种情况:

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

从前面的文章(《(二)Flutter 学习之 Dart 展开操作符和 Control Flow Collections》)[chiclaim.blog.csdn.net/article/det…] 我们知道 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,
    ...
)

运行效果如下图所示:

Flutter-BuilderFuture

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, 具体如何创建可以查看我之前的文章 《(六)Flutter 学习之 Dart 异步操作详解》

联系我

下面是我的公众号,干货文章不错过,有需要的可以关注下,有任何问题可以联系我, 也可以在文章下面给我留言:

公众号:  chiclaim