Flutter的数据传递/状态管理 一文搞定

1,841 阅读10分钟

不得不说,掘金的文章目录功能真的很方便。

Flutter,或者说app,数据传递,是难以避免的事情。页面内部之间,页面与页面之间,张三和李四,两者懂事需要说一些悄悄话。

这件事,我们有时叫 数据传递,有时候叫做状态管理,这随便啦。反正就是要传递下数据。


Flutter的状态管理,分为2种:

  • 1、局部状态
  • 2、全局状态

局部状态:一个StatefulWidget内部之间的事情,用setState解决,这点就不展开说了。
全局状态:整个app很多页面都需要用到的状态,比如是否登录了,用户名、用户id等。全局状态的管理的方式有好几种,比如 :

`InheritedWidget`
`StreamBuilder``ValueListenableBuilder`

另外还有有一些依赖库,比如google官方推荐的`Provider`
  • 利用 InheritedWidget ,我们可以实现 从上向下 的数据传递。
  • 利用 Notification 我们可以实现 从下向上 的数据传递
  • 利用 ValueListenableBuilder 实现,与方向无关,只要数据源发生变化它就会重新构建子组件树,因此可以实现任意流向的数据共享,

但是很多时候,我们需要异步UI更新,这就需要用到 FutureBuilderStreamBuilder

一、InheritedWidget 从上向下 传递

(只能从上向下,从下往上一般用的是:Notification

🌰栗子来了,比如官方的主题管理和Local语言环境,用的是InheritedWidget来实现的。这个是不是很“从上往下”。(是的,自问自答,自投自抢)

inherited 这个单词本身,就有 ’继承‘ 和 ’遗传‘ 的意思。

一、1、 InheritedWidget 要义

  • 1、 使用 InheritedWidget 作为全局状态的管理者,那么将InheritedWidget作为根Widget可以实现下面的Widget都可以获取到该Widget持有的状态。
  • 2、我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据!

看看InheritedWidget 类

这个可以先忽略,回头在看

/// 抽象类,继承自Proxywidget 继承路径InheritedWidget => ProxyWidget => Widget
abstract class InheritedWidget extends ProxyWidget {
  /// 构造函数
  /// 因为InheritedWidget是没有界面的Widget,所有需要传入实际的Widget	
  const InheritedWidget({ Key key, Widget child })
    : super(key: key, child: child);

  /// 重写了超类Widget createElement方法
  @override
  InheritedElement createElement() => InheritedElement(this);

  /// 父级或祖先widget中改变(updateShouldNotify返回true)时会被调用。
  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

一.2、 来个例子

功能:登录小例子,页面之间,根据是否登录,做出对应的操作

.
.

分解1 继承自InheritedWidget,构建of,复写 updateShouldNotify

  • of方法: 定义一个便捷方法,方便子树中的widget获取共享数据
  • updateShouldNotify方法:该回调决定当data发生变化时,是否通知子树中依赖data的Widget
import 'package:flutter/material.dart';

class LoginState {
  bool isLogin = false;

  bool operator ==(other) {
    return isLogin == (other as LoginState).isLogin;
  }
}

/*
InheritedWidget持有LoginState对象;
另外需要提供of静态方法,方法实现是context.inheritedFromWidgetOfExactType;

updateShouldNotify方法是用来判断当该Widget变化时,
这里面如果LoginState变化了,即isLogin参数变化了
才会去通知那些依赖该Widget的Widget变化。
*/
class LoginStateWidget extends InheritedWidget {
  final LoginState loginState;

  const LoginStateWidget(
      {Key? key, required this.loginState, required Widget child})
      : super(key: key, child: child);

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static LoginStateWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType(aspect: LoginStateWidget);
  }

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  @override
  bool updateShouldNotify(LoginStateWidget oldWidget) {
    return oldWidget.loginState == this.loginState;
  }
}

.
.

分解2、 InheritedWidget 作为 根Widght,包裹 MaterialApp 这很重要!

LoginStateWidget(继承自InheritedWidget) 作为 根Widght,并且传入了一个初始的LoginState。

void main(){
  runApp(MyApp());
}

class MyApp extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return LoginStateWidget(
        loginState: LoginState(),
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: new Text('Inherited测试'),
            ),
            body: MainPage(),
          ),
        ));
  }
}

分解3 添加两个页面,一个MainPage,一个LoginPage

核心操作:build里面,通过的LoginStateWidget的静态 of 方法 拿到共享数据 LoginState loginState = LoginStateWidget.of(context)!.loginState;

MainPage 页面有两个按钮,模拟登录成功,按下不同按钮,都会修改共享得到的值。
Login页面会根据被MainPage页面修改的值,然后显示不同的文字

// 假设app默认进入主页
class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 关键一步,通过的LoginStateWidget的静态 of 方法 拿到共享数据
    LoginState loginState = LoginStateWidget.of(context)!.loginState;
    print('MainPage loginState isLogin 值:${loginState.isLogin}');
    return Container(

      child: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text('MainPage 页', textScaleFactor: 3),

            MaterialButton(
              onPressed: () {
                loginState.isLogin = true;
                Navigator.push(context, MaterialPageRoute(builder: (context) {
                  return LoginPage();
                }));

              },
              // 从 loginState 中得到 isLogin 显示不同的文字
              child: Text('模拟正确登录'),
            ),
            MaterialButton(
              onPressed: () {
                loginState.isLogin = false;
                Navigator.push(context, MaterialPageRoute(builder: (context) {
                  return LoginPage();
                }));
              },
              // 从 loginState 中得到 isLogin 显示不同的文字
              child: Text('模拟错误登录'),
            ),
            MaterialButton(
              onPressed: () {
                showDialog<String>(
                  context: context,
                  builder: (BuildContext context) {
                    return SimpleDialog(
                      title: const Text('结果'),
                      children: <Widget>[
                        SimpleDialogOption(
                          child: Text('当前isLogin为: ${loginState.isLogin}'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ],
                    );
                  },
                );

              },
              // 从 loginState 中得到 isLogin 显示不同的文字
              child: Text('当前的jsLogin的值得'),
            )
          ],
        ),
      ),
    );
  }
}

// 假设在登录页之后,会显示登录之后结果,根据登录成功或者失败,显示对应的按钮
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 这个子Wight也通过 LoginStateWidget 的of方法拿到 共享数据
    LoginState loginState = LoginStateWidget.of(context)!.loginState;
    print('LoginPage loginState 值:${loginState.isLogin}');


    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('LoginPage'),
        ),
        body: Container(
          color: Colors.grey,
          child: Center(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Text('LoginPage 页', textScaleFactor: 3),

                MaterialButton(
                  onPressed: () {
                    print('LoginPage pop之前:${loginState.isLogin}');
                    loginState.isLogin = !loginState.isLogin;
                    Navigator.pop(context);
                    print('LoginPage pop之后:${loginState.isLogin}');
                  },
                  child: Text(loginState.isLogin ? '退出登录' : '登录'),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

.
.

来份完整的代码吧。

按道理这些Page应该写在不同的文件好一些,但是这里为了方便展示,先到一块去了

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return LoginStateWidget(
        loginState: LoginState(),
        child: MaterialApp(
          home: Scaffold(
            appBar: AppBar(
              title: new Text('Inherited测试'),
            ),
            body: MainPage(),
          ),
        ));
  }
}

class LoginState {
  bool isLogin = false;

  bool operator ==(other) {
    return isLogin == (other as LoginState).isLogin;
  }
}

/*
InheritedWidget持有LoginState对象;
另外需要提供of静态方法,方法实现是context.inheritedFromWidgetOfExactType;

updateShouldNotify方法是用来判断当该Widget变化时,
这里面如果LoginState变化了,即isLogin参数变化了
才会去通知那些依赖该Widget的Widget变化。
*/
class LoginStateWidget extends InheritedWidget {
  final LoginState loginState;

  const LoginStateWidget(
      {Key? key, required this.loginState, required Widget child})
      : super(key: key, child: child);

  //定义一个便捷方法,方便子树中的widget获取共享数据
  static LoginStateWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType(aspect: LoginStateWidget);
  }

  //该回调决定当data发生变化时,是否通知子树中依赖data的Widget
  @override
  bool updateShouldNotify(LoginStateWidget oldWidget) {
    return oldWidget.loginState == this.loginState;
  }
}

// 假设app默认进入主页
class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 关键一步,通过的LoginStateWidget的静态 of 方法 拿到共享数据
    LoginState loginState = LoginStateWidget.of(context)!.loginState;
    print('MainPage loginState isLogin 值:${loginState.isLogin}');
    return Container(

      child: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            Text('MainPage 页', textScaleFactor: 3),

            MaterialButton(
              onPressed: () {
                loginState.isLogin = true;
                Navigator.push(context, MaterialPageRoute(builder: (context) {
                  return LoginPage();
                }));

              },
              // 从 loginState 中得到 isLogin 显示不同的文字
              child: Text('模拟正确登录'),
            ),
            MaterialButton(
              onPressed: () {
                loginState.isLogin = false;
                Navigator.push(context, MaterialPageRoute(builder: (context) {
                  return LoginPage();
                }));
              },
              // 从 loginState 中得到 isLogin 显示不同的文字
              child: Text('模拟错误登录'),
            ),
            MaterialButton(
              onPressed: () {
                showDialog<String>(
                  context: context,
                  builder: (BuildContext context) {
                    return SimpleDialog(
                      title: const Text('结果'),
                      children: <Widget>[
                        SimpleDialogOption(
                          child: Text('当前isLogin为: ${loginState.isLogin}'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ],
                    );
                  },
                );

              },
              // 从 loginState 中得到 isLogin 显示不同的文字
              child: Text('当前的jsLogin的值得'),
            )
          ],
        ),
      ),
    );
  }
}

// 假设在登录页之后,会显示登录之后结果,根据登录成功或者失败,显示对应的按钮
class LoginPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 这个子Wight也通过 LoginStateWidget 的of方法拿到 共享数据
    LoginState loginState = LoginStateWidget.of(context)!.loginState;
    print('LoginPage loginState 值:${loginState.isLogin}');


    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('LoginPage'),
        ),
        body: Container(
          color: Colors.grey,
          child: Center(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                Text('LoginPage 页', textScaleFactor: 3),

                MaterialButton(
                  onPressed: () {
                    print('LoginPage pop之前:${loginState.isLogin}');
                    loginState.isLogin = !loginState.isLogin;
                    Navigator.pop(context);
                    print('LoginPage pop之后:${loginState.isLogin}');
                  },
                  child: Text(loginState.isLogin ? '退出登录' : '登录'),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

效果:

按下 模拟正确登录

  • 按下: “模拟正确登录”
  • 按下 “退出登录” 把isLogin位置为false
  • 测试状态显示为 false

image.png

.
.

按下 模拟错误登录

  • 按下: “模拟错误登录”
  • 按下 “登录” 把isLogin位置为true
  • 测试状态显示为 true

image.png

应该非常清晰明了吧。

  • MainPage通过共享数据改变了isLogin的状态

  • LoginPage按下不同的按钮,也通过共享数据改变了isLogin的状态

.
.

一.3、InheritedWidget 和 didChangeDependencies

  • 在之前介绍StatefulWidget时,我们提到State对象有一个didChangeDependencies回调,它会在“依赖”发生变化时被Flutter 框架调用。而这个“依赖”指的就是子 widget 是否使用了父 widget 中InheritedWidget的数据!如果使用了,则代表子 widget 有依赖;如果没有使用则代表没有依赖。

  • 也就是说,只有当state的build方法获取了共享数据值(通过 dependOnInheritedElement 的方式),子widget的 didChangeDependencies 方法才会调用,这个我们重新 didChangeDependencies 输出个日志尝试一下就知道

  • 这种机制可以使子组件在所依赖的InheritedWidget变化时来更新自身!比如当主题、locale(语言)等发生变化时,依赖其的子 widget 的didChangeDependencies方法将会被调用。

一.4、如何判断Widget是否为 InheritedWidget 的子Widget呢?

  • 就看是否调用了 dependOnInheritedWidgetOfExactType() 方法

其实真正注册依赖关系的方法是 dependOnInheritedElement ,只不过 dependOnInheritedWidgetOfExactType 又调用了 dependOnInheritedElement。

二、InheritedWidget 的缺点

一个InheritedWidget,如果存在多个子节点,有时候我们只想改变一个子节点。可是当某个子节点setSate的时候,很容易造成全局build。这是一种资源浪费。

比较好的解决办法是使用缓存,而Google的 Provider 库就考虑到了这个问题。
其实很多三方数据共享库也解决了这个问题,但是不管是官方还是三方,本质上,基本都是对 InheritedWidget 的封装。

看源码

  @override
  InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }

一.5、如何只使用共享数据,但是不调用 didChangeDependencies 方法呢?

我们只需要将ShareDataWidget.of()的实现改一下即可

//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget of(BuildContext context) {
  //return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
  return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
  • 也就是把 dependOnInheritedWidgetOfExactType 改成 getElementForInheritedWidgetOfExactType

我们可以看到,dependOnInheritedWidgetOfExactType() 比 getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,dependOnInheritedElement

@override
InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  return ancestor;
}
@override
InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
  //多出的部分
  if (ancestor != null) {
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  }
  _hadUnsatisfiedDependencies = true;
  return null;
}

一.6、 手动写一个 Provider

Provider是Google的一个三方库,做状态管理的。
日和使用网上有大量的文章。

我们可以通过自己简单写一个,来明白他的原理。

这里有个文章,还是相当不错的。

跨组件状态共享(Provider) book.flutterchina.club/chapter7/pr…

三、Notification 从下向上传递

我们说了 InheritedWidget 是 从上向下 的,而 从下向上, 一般用的是 Notification。

子节点状态变更,向上上报通过发送通知的方式

  • 定义通知类,继承自Notification
  • 父节点使用NotificationListener进行监听捕获通知
  • 子节点有数据变更,调用接口进行上报Notification(data).dispatch(context)

来个例子

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: new Text('Notify测试'),
        ),
        body: NotificationRoute(),
      ),
    );
  }
}

// 定义通知类,继承自Notification
class MyNotification extends Notification {
  MyNotification(this.msg);
  final String msg;
}

class NotificationRoute extends StatefulWidget {
  @override
  NotificationRouteState createState() {
    return NotificationRouteState();
  }
}

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //父节点使用NotificationListener进行监听捕获通知
    return NotificationListener<MyNotification>(
      onNotification: (notification) {
        setState(() {
          _msg+=notification.msg+"  ";
        });
        return true;
      },
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
//           ElevatedButton(
//           onPressed: () => MyNotification("Hi").dispatch(context),
//           child: Text("Send Notification"),
//          ),
            Builder(
              builder: (context) {
                return ElevatedButton(
                  //按钮点击时分发通知
                  onPressed: () {
                    // 发出通知,进行分发
                    MyNotification("Hi").dispatch(context);
                  },
                  child: Text("Send Notification"),
                );
              },
            ),
            Text(_msg)
          ],
        ),
      ),
    );
  }
}

.
.

效果 按下按钮,添加多一个 Hi

image.png

所谓冒泡

  • 自下向上
  • 层层分发
  • 消息传递

监听嵌套和 阻止冒泡

如果有多个监听,正常情况下(onNotification中返回true),会一层一层往外传,多个监听都收到。这个是正常的冒泡

那么,我们可不可以阻止冒泡呢?

答案是可以的。

其实也就是 onNotification 返回 false,表示阻止冒泡,不往外分发了。自己消费掉了。

class NotificationRouteState extends State<NotificationRoute> {
  String _msg="";
  @override
  Widget build(BuildContext context) {
    //监听通知
    return NotificationListener<MyNotification>(
      onNotification: (notification){
        print(notification.msg); //打印通知
        return false;
      },
      child: NotificationListener<MyNotification>(
        onNotification: (notification) {
          setState(() {
            _msg+=notification.msg+"  ";
          });
          return false; 
        },
        child: ...//省略重复代码
      ),
    );
  }
}

上列中两个NotificationListener进行了嵌套,子NotificationListeneronNotification回调返回了false,表示不阻止冒泡,所以父NotificationListener仍然会受到通知,所以控制台会打印出通知信息;如果将子NotificationListeneronNotification回调的返回值改为true,则父NotificationListener便不会再打印通知了,因为子NotificationListener已经终止通知冒泡了。

四、ValueListenableBuilder

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

const ValueListenableBuilder({
  Key? key,
  required this.valueListenable, // 数据源,类型为ValueListenable<T>
  required this.builder, // builder
  this.child,
}
  • valueListenable:类型为 ValueListenable<T>,表示一个可监听的数据源。
  • 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,
      ),
    );
  }
}

运行后连续点击两次 + 按钮效果如下:

image.png

可以看见,功能正常实现了,同时控制台只在页面打开时 build 了一次,点击 + 按钮的时候只是ValueListenableBuilder 重新构建了子组件树,而整个页面并没有重新 build ,因此日志面板只打印了一次 "build" 。因此我们有一个建议就是:尽可能让 ValueListenableBuilder 只构建依赖数据源的widget,这样的话可以缩小重新构建的范围,也就是说 ValueListenableBuilder 的拆分粒度应该尽可能细

关于 ValueListenableBuilder 有两点需要了解:

  1. 和数据流向无关,可以实现任意流向的数据共享。
  2. 实践中,ValueListenableBuilder 的拆分粒度应该尽可能细,可以提高性能。

五、异步更新UI FutureBuilderStreamBuilder

很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面;又比如我们想展示Stream(比如文件流、互联网数据接收流)的进度。当然,通过 StatefulWidget 我们完全可以实现上述这些功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,因此Flutter专门提供了FutureBuilderStreamBuilder两个组件来快速实现这种功能。

五.1、FutureBuilder

FutureBuilder会依赖一个Future,通常在Future里面做异步耗时操作,在 builder 里面更新UI。

FutureBuilder构造函数

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

snapshot会包含当前异步任务的状态信息及结果信息 ,比如我们可以通过snapshot.connectionState获取异步任务的状态信息、通过snapshot.hasError判断异步任务是否有错误等等,完整的定义读者可以查看AsyncSnapshot类定义。

另外,FutureBuilderbuilder函数签名和StreamBuilderbuilder是相同的。

例子

我们实现一个路由,当该路由打开时我们从网上获取数据,获取数据时弹一个加载框;获取结束时,如果成功则显示获取到的数据,如果失败则显示错误。

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

FutureBuilder使用代码如下:

...
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();
        }
      },
    ),
  );
}

image.png

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

上面代码中我们在builder中根据当前异步任务状态ConnectionState来返回不同的widget。ConnectionState是一个枚举类,定义如下:

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

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

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

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

注意,ConnectionState.active只在StreamBuilder中才会出现

五.2、 StreamBuilder

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

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

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

示例

我们创建一个计时器的示例:每隔1秒,计数加1。这里,我们使用Stream来实现每隔一秒生成一个数字:

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

StreamBuilder使用代码如下

 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
      },
    );
 }

注意,本示例只是为了演示StreamBuilder的使用,在实战中,凡是UI会依赖多个异步数据而发生变化的场景都可以使用StreamBuilder

就写到这里吧。


参考:

Flutter状态管理(1)——InheritedWidget
cloud.tencent.com/developer/a…

Flutter 数据传输 cloud.tencent.com/developer/a…

Flutter状态管理(2)——单Stream和广播Stream
cloud.tencent.com/developer/a…

通知 Notification
book.flutterchina.club/chapter8/no…

Flutter状态管理(2)——单Stream和广播Stream
cloud.tencent.com/developer/a…

异步UI更新(FutureBuilder、StreamBuilder) book.flutterchina.club/chapter7/fu…