flutter异步UI更新(FutureBuilder、StreamBuilder)

458 阅读2分钟

异步UI更新

很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示Stream(比如文件流、互联网数据接收流)的进度。

之前已经介绍过 Dart 中的异步操作主要是 Future 和 Stream,也介绍了相关的用法,这片文章将介绍与之对应的构建异步UI的两个方法 FutureBuilder 与 StreamBuilder。

FutureBuilder

Future 必须在更早之前获取,例如在State.initState、State.didUpdateWidget 或 State.didChangeDependencies 期间得到。它不能在构造 FutureBuilder 时调用 State.build 或 StatelessWidget.build 方法时创建。如果 Future 是与 FutureBuilder 同时创建的,那么每当 FutureBuilder 的父节点重建时,异步任务就会重新启动。

构建方法:

const FutureBuilder<T>({
Key? key,
required Future<T>? future, // FutureBuilder依赖的Future,通常是一个异步耗时任务
T? initialData, // 初始数据,用户设置默认数据
required AsyncWidgetBuilder<T> builder // Widget构建器;该构建器会在Future执行的不同阶段被多次调用
})

AsyncWidgetBuilder<T> = Widget Function(
    BuildContext context,
    AsyncSnapshot<T> snapshot
)

snapshot会包含当前异步任务的状态信息及结果信息 ,比如我们可以通过snapshot.connectionState获取异步任务的状态信息, 通过snapshot.hasError判断异步任务是否有错误等等,完整的定义读者可以查看AsyncSnapshot类定义。另外,FutureBuilder的builder函数签名和StreamBuilder的builder是相同的。

查看源码可知 snapshot.connectionState 属性是一个枚举,用于表示异步操作的连接状态:

  • ConnectionState.none:表示异步操作未启动,或者 Future 对象尚未创建。
  • ConnectionState.waiting:表示异步操作正在进行中,等待 Future 完成。
  • ConnectionState.active:(用于Stream)表示数据流处于活动状态,已经开始发送事件。在接收到第一个事件之后,connectionState 的值将变为 active,但是还没有完成。
  • ConnectionState.done:表示异步操作已经完成或抛出了异常。

可以根据状态返回不同的UI:

FutureBuilder<String>(
  future: myFuture,
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    } else if (snapshot.connectionState == ConnectionState.done) {
      if (snapshot.hasData) {
        return Text('Data: ${snapshot.data}');
      } else if (snapshot.hasError) {
        return Text('Error: ${snapshot.error}');
      }
    }

    return Text('Press the button to start');
  },
)

完整代码示例

  // 以下代码在 State 中
    
  late Future<List<Album>> futureAlbums;

  // 这是一段从接口获取数据,并转换成 List<Album> 的方法
  Future<List<Album>> fetchAlbum() async {
    final response = await http.get(
      Uri.parse('https://jsonplaceholder.typicode.com/albums'),
      headers: {
        'Content-Type': 'application/json; charset=UTF-8',
        HttpHeaders.authorizationHeader: 'Basic your_api_token_here',
      },
    );

    if (response.statusCode == 200) {
      // 如果服务器返回了 200 OK 响应,则解析 JSON。
      final List<dynamic> jsonList = jsonDecode(response.body);
      return jsonList.map((json) => Album.fromJson(json)).toList();
    } else {
      // 如果服务器未返回 200 OK 响应,则抛出异常。
      throw Exception('Failed to load album');
    }
  }
  
  @override
  void initState() {
    super.initState();
    futureAlbums = fetchAlbum(); // 在 initState 时获取 Future
  }
  
  ...
  
  Widget build(BuildContext context) {
    return Center(
       child: FutureBuilder<List<Album>>(
              future: futureAlbums,
              builder: (context, AsyncSnapshot snapshot) {
                // 有数据
                if (snapshot.hasData) {
                  return Scrollbar(
                      child: ListView.builder(
                    itemCount: snapshot.data!.length,
                    itemBuilder: (BuildContext context, int index) {
                      return ListTile(
                        title: Text(snapshot.data![index].title),
                      );
                    },
                  ));
                  // 发生错误
                } else if (snapshot.hasError) {
                  return Text('${snapshot.error}');
                }
                // 默认情况下展示一个加载中的进度条
                return const CircularProgressIndicator();
              },
            );
    );
  }

StreamBuilder

我们知道,在Dart中Stream 也是用于接收异步事件数据,和Future 不同的是,它可以接收多个异步操作的结果,它常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。StreamBuilder正是用于配合Stream来展示流上事件(数据)变化的UI组件。

构建方法:

const StreamBuilder<T>({
    Key? key,
    T? initialData, // 初始数据,用户设置默认数据
    required Stream<T>? stream, // 依赖的一个 Stream
    required AsyncWidgetBuilder<T> builder // 与 FutureBuilder 相同,只是 connectionState 中的 active 状态需要特殊注意
})

创建一个异步数据流,每隔1秒发送一个数据,获取10个数字(0-9)。

// 以下代码在 State 中
final Stream<int> counterStream = Stream.periodic(const Duration(seconds: 1), (count) => count).take(10);

final broadcastStreamController = StreamController<int>.broadcast(); // 广播类型的 Stream,可以被多个监听

@override
  void initState() {
    super.initState();
    futureAlbums = fetchAlbum();
    // pipe() 是 Stream 类的一个方法,用于将一个流连接到另一个流。它会将源流中的事件转发到目标流中,以实现数据传递
    counterStream.pipe(broadcastStreamController);
}

Widget build(BuildContext context) {
   return Center(
     child: StreamBuilder<int>(
              stream: broadcastStreamController.stream,
              builder: (context, AsyncSnapshot<int> snapshot) {
                switch (snapshot.connectionState) {
                  case ConnectionState.waiting:
                    return const Text('waiting');
                  case ConnectionState.none:
                    return const Text('none');
                  case ConnectionState.active:
                    // 每次数据发送都会重新 build,更新ui
                    return Text('Count: ${snapshot.data}');
                  case ConnectionState.done:
                    return const Text('done');
                }
              },
            );
    
   );
}