Flutter-ValueListenaleBuilder&异步更新UI

66 阅读1分钟

ValueListenableBuilder

InheritedWidget提供一种在Widget树中从上到下共享数据的方式,但是很多场景的数据流向并不是从上到下,也有横向或从下到上等方式。为了解决这个问题,Flutter提供了一个ValueListenableBuilder组件,它的功能是监听一个数据源,如果数据源发生变化,则会重新执行builder:

const ValueListenableBuilder({
  Key? key,
  required this.valueListenable, // 数据源,类型为ValueListenable<T>
  required this.builder, // builder
  this.child,
} 
  • valueListenable:类型为ValueListenable,表示一个可监听的数据源。
  • builder:数据源发生变化通知时,会重新调用builder重新build子组件树。
  • child:builder中每次都会重新构建整个子组件树,如果子组件树中有一些不变的部分,可以传递给child,child会作为builder的第三个参数传递给builder,通过这种方式就可以实现组件缓存,原理和AnimatedBuilder第三个child相同。

ValueListenableBuilder和数据流向无关,只要整个数据源发生变化,它就会重新构建子组件树,可以实现任意流向的数据共享。

实例:

class ValueListenableRoute extends StatefulWidget {
  const ValueListenableRoute({Key? key}) : super(key: key);

  @override
  State<ValueListenableRoute> createState() => _ValueListenableState();
}

class _ValueListenableState extends State<ValueListenableRoute> {
  // 定义一个ValueNotifier,当数字变化时会通知 ValueListenableBuilder
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);
  static const double textScaleFactor = 1.5;

  @override
  Widget build(BuildContext context) {
    // 添加 + 按钮不会触发整个 ValueListenableRoute 组件的 build
    print('build');
    return Scaffold(
      appBar: AppBar(title: Text('ValueListenableBuilder 测试')),
      body: Center(
        child: ValueListenableBuilder<int>(
          builder: (BuildContext context, int value, Widget? child) {
            // builder 方法只会在 _counter 变化时被调用
            return Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                child!,
                Text('$value 次',textScaleFactor: textScaleFactor),
              ],
            );
          },
          valueListenable: _counter,
          // 当子组件不依赖变化的数据,且子组件收件开销比较大时,指定 child 属性来缓存子组件非常有用
          child: const Text('点击了 ', textScaleFactor: textScaleFactor),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        // 点击后值 +1,触发 ValueListenableBuilder 重新构建
        onPressed: () => _counter.value += 1,
      ),
    );
  }
}

建议:尽可能让ValueListenableBuilder只构建依赖数据源的widget,这样的话可以缩小重新构建的范围,也就是ValueListenableBuilder的拆分粒度应该尽可能细。

  • 和数据流向无关,可以实现任意流向的数据共享。
  • 实践中,ValueListenableBuilder的粒度尽可能细,提供性能。

异步更新UI-FutureBuilder

FutureBuilder会依赖一个Future,它会根据所依赖的Future的状态来动态构建自身:

FutureBuilder({
  this.future,
  this.initialData,
  required this.builder,
})
  • future:FutureBuilder依赖的Future,通常是一个异步耗时任务。
  • initialData:初始数据,用户设置默认数据。
  • builder:Widget构建器,在Future执行的不同阶段被多次调用,构建器签名如下:
Function (BuildContext context, AsyncSnapshot snapshot) 

snapshot会包含当前异步任务的状态信息及结果信息,比如通过snapshot.connectionState获取异步任务的状态信息、通过snapshot.hasError判断异步任务是否有错误等。 FlutureBuilder的builder函数签名和StreamBuilder的builder相同。

实例:

Future<String> mockNetworkData() async {
  return Future.delayed(Duration(seconds: 2), () => "我是从互联网上获取的数据");
}

Widget build(BuildContext context) {
  return Center(
    child: FutureBuilder<String>(
      future: mockNetworkData(),
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        // 请求已结束
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasError) {
            // 请求失败,显示错误
            return Text("Error: ${snapshot.error}");
          } else {
            // 请求成功,显示数据
            return Text("Contents: ${snapshot.data}");
          }
        } else {
          // 请求未结束,显示loading
          return CircularProgressIndicator();
        }
      },
    ),
  );
}

注意:实例代码中,每次组件重新build都会重新发起请求,每次future都是新构建的,实践中通常会有一些缓存策略,常见的处理方式是在future成功后将future缓存,这样下次build时,就不会再重新发起异步任务。

builder中根据当前异步任务状态ConnectionState来返回不同的widget。ConnectionState是一个枚举类:

enum ConnectionState {
  /// 当前没有异步任务,比如[FutureBuilder]的[future]为null时
  none,

  /// 异步任务处于等待状态
  waiting,

  /// Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态。
///仅在StreamBuilder中才有
  active,

  /// 异步任务已经终止.
  done,
}

StreamBuilder

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

StreamBuilder({
  this.initialData,
  Stream<T> stream,
  required this.builder,
}) 

可以看到和FutureBuilder的构造函数只有一点不同:前者需要Future后者需要stream。

实例:

Stream<int> counter() {
  return Stream.periodic(Duration(seconds: 1), (i) {
    return i;
  });
}

Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: counter(), //
      //initialData: ,// a Stream<int> or null
      builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
        if (snapshot.hasError)
          return Text('Error: ${snapshot.error}');
        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return Text('没有Stream');
          case ConnectionState.waiting:
            return Text('等待数据...');
          case ConnectionState.active:
            return Text('active: ${snapshot.data}');
          case ConnectionState.done:
            return Text('Stream 已关闭');
        }
        return null; // unreachable
      },
    );
 }