原文:FutureBuilder

Widget that builds itself based on the snapshot of interaction with a Future.

官方的意思是一个基于与Future交互快照构建自身的小组件。

FutureBuilder用法和实现

构造方法:

1
2
3
4
5
6
7
 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在时间轴上执行过程的状态快照。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//FutureBuilder控件
new FutureBuilder<String>(
  future: _calculation, // 用户定义的需要异步执行的代码,类型为Future<String>或者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暴露的快照属性,定义好对应状态下的处理代码,即可实现异步执行时的交互逻辑。

看似有些绕口,我们看下面这段代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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<String>或者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就是异步函数_getData的执行状态,用户通过定义在Connectionstate.noneConnectionState.waiting状态下,输出一个TextCenter显示并且内置文字CircularProgressIndicator的组件,其意义即:当异步函数_getData未执行时,屏幕正中央显示文字:还没有开始网络请求。和正在执行时,显示一个刷新状态的组件。

_getData执行完毕时,snaphot.connectionState的值变为ConnectionState.done,此时即可输出根据HTTP请求到的数据对应的ListItem。由于ConnectState.done是除了ConnectionState.nodeConnectionState.waiting以外的唯一值,所以代码中在switch下用default也可以(ConnectionState.active好像整个过程中没有调用)。

由于通过FutureBuilder内的builder()函数即可草所控件的状态和重绘,我们不必通过自己写异步状态的判断和多次使用setState()实现页面上加载中和加载完成显示效果的切换,因为FutureBuilder内部自带执行了setState()的方法。

现在一个FutureBuilder的构建就算完成了。

防止FutureBuilder进行不必要的重绘

如果姿势写一个FutureBuilder,我们就不需要floatingActionButton里面一系列东西,所以这时候就到它的出场了。 代码中意思,每次点击它,就在我们标题后面加一个”.“,看一下效果 重绘 确实是改变了标题,但是整个页面也随着setState而进行了不必要的重绘。

即使AppBar和FutureBuilder没有任何关联,每次我们改变它的值(通过调用setState),FuterBuilder都会再次经历整个整个生命周期,它重新拉去future,导致不必要的流量,并再次显示负载,导致糟糕的用户体验。

这个问题以各种方式变现出来。某些情况下,它甚至不像上面的例子那么明显。例如:

  • 从当前不在屏幕上的页面生成的网络流量
  • 热重装不能正常工作
  • 更新某些继承的窗口小部件中的值时,丢失导航器状态
  • 等等

原因:didUpdateWidget问题

如果我们仔细看代码FutureBuilder,我们会发现它是一个StatefulWidget。我们知道,StatefulWidget维护一个长期存在的State对象。这种状态有一些管理其声明周期的方法,就像initStatebuilddidUpdateWidget

initState在第一次创建状态对象时只调用一次,build在每次我们需要构建显示的窗口小部件时调用它,didUpdateWidget只要附加此State对象的窗口小部件发生变化时,就会调用此方法。

FutureBuilder这种情况下,这个方法看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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实例,则会重复所有的内容:取消订阅,并再次订阅。