七、数据共享
InheritedWidget父子数据共享
InheritedWidget是 Flutter 中非常重要的一个功能型组件,它提供了一种在 widget 树中从上到下共享数据的方式,比如我们在应用的根 widget 中通过InheritedWidget共享了一个数据,那么我们便可以在任意子widget 中来获取该共享的数据!这个特性在一些需要在整个 widget 树中共享数据的场景中非常方便!如Flutter SDK中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。
InheritedWidget和 React 中的 context 功能类似,和逐级传递数据相比,它们能实现组件跨级传递数据。InheritedWidget的在 widget 树中数据传递方向是从上到下的,这和通知Notification(将在下一章中介绍)的传递方向正好相反。
示例
import 'package:flutter/material.dart';
import './components/InheritedGrandSonWidget.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatefulWidget {
  
  _MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
  String sharedData = "Hello from MyApp";
  
  Widget build(BuildContext context) {
    // 2. 在页面中使用 `MyInheritedWidget` 并向其提供共享数据
    return MyInheritedWidget(
      sharedData: sharedData,
      child: MaterialApp(
        home: Scaffold(
          appBar: AppBar(
            title: Text('InheritedWidget Example'),
          ),
          body: ChildWidget(),
          floatingActionButton: FloatingActionButton(
            onPressed: () {
              setState(() => {sharedData = DateTime.timestamp().toString()});
            },
            child: Icon(Icons.add),
          ),
        ),
      ),
    );
  }
}
// 1. `MyInheritedWidget` 持有一个名为 `sharedData` 的字符串,将这个字符串共享给所有访问它的子widget
class MyInheritedWidget extends InheritedWidget {
  final String sharedData;
  MyInheritedWidget({
    Key? key,
    required this.sharedData,
    required Widget child,
  }) : super(key: key, child: child);
  // 定义一个便捷方法,方便子树中的widget获取共享数据
  static MyInheritedWidget? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
  }
  // 判断是否需要更新
  
  bool updateShouldNotify(MyInheritedWidget oldWidget) { // 5. 如果返回true则会触发后代组件didChangeDependencies钩子执行,固定返回false也不影响build的界面变化
    return oldWidget.sharedData != sharedData;
  }
}
class ChildWidget extends StatefulWidget {
  
  _ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State {
  
  void didChangeDependencies() { // 4. 如果build中依赖MyInheritedWidget中的数据才会被调用
    super.didChangeDependencies();
    print('didChangeDependencies');
  }
  
  Widget build(BuildContext context) {
    return Column(children: [
      Text(MyInheritedWidget.of(context)!.sharedData), // 3. 在子widget中访问共享数据
      GrandSonWidget()
    ]);
  }
}
// 孙子组件: ./components/InheritedGrandSonWidget.dart
import 'package:flutter/material.dart';
import '../main.19.状态共享InheritedWidget.dart';
class GrandSonWidget extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Text('孙子组件:' + MyInheritedWidget.of(context)!.sharedData);
  }
}
注意:
- 首先,定义一个扩展自 InheritedWidget的类,该类将持有要共享的数据。
- 在页面中使用 MyInheritedWidget并向其提供共享数据
- 在任何子(后代)widget中,都可以通过调用 MyInheritedWidget.of(context)来获取共享的数据

数据更新钩子(didChangeDependencies)
在之前介绍StatefulWidget时,我们提到State对象有一个didChangeDependencies回调,它会在“依赖”发生变化时被Flutter 框架调用。而这个“依赖”指的就是子 widget 是否使用了父 widget 中InheritedWidget的数据!如果使用了,则代表子 widget 有依赖;如果没有使用则代表没有依赖。
当 widget 第一次构建时,didChangeDependencies 会在 initState 之后立即被调用:
 此外,每当该 widget 的父级或祖先 widget 中的
此外,每当该 widget 的父级或祖先 widget 中的 InheritedWidget 发生变化时,didChangeDependencies 也会被调用。
注意:
4. 如果build中没有依赖MyInheritedWidget中的数据,didChangeDependencies不会被调用
5. MyInheritedWidget中updateShouldNotify, 如果返回true则会触发后代组件didChangeDependencies钩子执行,固定返回false也不影响build的界面变化
使用场景
一般来说,子 widget 很少会重写此方法,因为在依赖改变后 Flutter 框架也都会调用build()方法重新构建组件树
- 如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次build()都执行这些昂贵操作。
- 订阅 InheritedWidget: 如果 widget 依赖于 InheritedWidget中的数据,可以在didChangeDependencies中订阅这些数据。当数据发生变化时,didChangeDependencies会被调用,在这里更新 widget 以响应这些变化。
- 读取上下文依赖的数据: 有些类,如 Theme或MediaQuery,也是通过InheritedWidget实现的。当你需要根据这些类的数据来构建你的 widget 时,didChangeDependencies是一个读取这些数据并相应地更新你的 widget 的好地方。
跨组件状态共享
事件总线eventBus
pub有现成的event_bus:
import 'package:event_bus/event_bus.dart';
import 'package:flutter/material.dart';
EventBus eventBus = EventBus();
class UserLoggedInEvent {
  final String username;
  UserLoggedInEvent(this.username);
}
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('EventBus Demo'),
        ),
        body: Column(
          children: [WidgetA(), WidgetB()],
        ),
      ),
    );
  }
}
class WidgetA extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        eventBus.fire(UserLoggedInEvent('Alice'));
      },
      child: Text('Log in as Alice'),
    );
  }
}
class WidgetB extends StatefulWidget {
  
  _WidgetBState createState() => _WidgetBState();
}
class _WidgetBState extends State<WidgetB> {
  String _username = '';
  
  void initState() {
    super.initState();
    eventBus.on<UserLoggedInEvent>().listen((event) {
      setState(() {
        _username = event.username;
      });
    });
  }
  
  Widget build(BuildContext context) {
    return Text('Logged in user: $_username');
  }
}
自己实现一个
- 定义 EventBus 类
typedef EventCallback = void Function(dynamic event);
class EventBus {
  // 用于保存单个事件类型的订阅者列表
  final _eventMap = Map<Type, List<EventCallback>>();
  // 添加订阅
  void on<T>(EventCallback callback) {
    if (_eventMap[T] == null) {
      _eventMap[T] = [];
    }
    _eventMap[T]!.add(callback);
  }
  // 移除订阅
  void off<T>(EventCallback callback) {
    _eventMap[T]?.remove(callback);
  }
  // 发布事件
  void fire<T>(T event) {
    _eventMap[event.runtimeType]?.forEach((callback) {
      callback(event);
    });
  }
}
// 创建全局的EventBus实例
final eventBus = EventBus();
- 定义事件
class UserLoggedInEvent {
  final String username;
  UserLoggedInEvent(this.username);
}
class UserLoggedOutEvent {}
- 订阅和发布事件
void main() {
  // 订阅登录事件
  eventBus.on<UserLoggedInEvent>((event) {
    print('User logged in: ${event.username}');
  });
  // 模拟用户登录
  eventBus.fire(UserLoggedInEvent('Alice'));
}
- 取消订阅
EventCallback<UserLoggedInEvent> callback = (event) {
  print('User logged in: ${event.username}');
};
// 订阅事件
eventBus.on<UserLoggedInEvent>(callback);
// 取消订阅事件
eventBus.off<UserLoggedInEvent>(callback);
Provider
Provider是Flutter官方出的状态管理包,写项目的时候再补使用方式吧。 更深的实现原理后续再看7.3 跨组件状态共享 | 《Flutter实战·第二版》
同级数据共享
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 和数据流向是无关的,只要数据源发生变化它就会重新构建子组件树,因此可以实现任意流向的数据共享。
用法
ValueListenableBuilder 非常适用于简单的局部UI更新场景,比如当一个变量改变时,只需要更新UI中的一小部分而不是整个屏幕。
class MyCounterWidget extends StatefulWidget {
  
  _MyCounterWidgetState createState() => _MyCounterWidgetState();
}
class _MyCounterWidgetState extends State<MyCounterWidget> {
  // 创建ValueNotifier对象
  final ValueNotifier<int> _counter = ValueNotifier<int>(0);
  void _incrementCounter() {
    _counter.value += 1; // 增加计数器的值
  }
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('ValueListenableBuilder Demo'),
      ),
      body: Center(
        // 使用ValueListenableBuilder来监听_value的变化
        child: ValueListenableBuilder<int>(
          valueListenable: _counter,
          builder: (BuildContext context, int value, Widget? child) {
            // 当_value发生变化时,这个部分会自动重建
            return Text('Button tapped $value time${value == 1 ? '' : 's'}.');
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
  
  void dispose() {
    _counter.dispose(); // 不要忘记释放ValueNotifier资源
    super.dispose();
  }
}
对比直接使用局部变量的优势
局部变量的局限性
- 局部变量不会触发重建:当局部变量的值改变时,它不会自动触发widget的重建。你需要手动调用setState来通知框架状态已经改变,从而导致widget重建。
- 状态共享的复杂性:如果你需要将局部变量的状态在widget树的不同部分之间共享,这会变得比较复杂。你可能需要将状态提升到共同的祖先widget中,然后通过回调函数或其他方式将状态传递给需要它的子widget。
- 状态更新的效率问题:使用setState更新局部变量会导致当前widget及其所有子widget的重建,这在复杂的widget树中可能会影响性能。
使用ValueListenable和ValueListenableBuilder的优势
- 细粒度的重建控制:ValueListenableBuilder仅在ValueListenable对象的值发生变化时重建其子树。这意味着只有真正依赖于这个值的widget会被重建,从而提高了效率。
- 简化的状态共享:使用ValueListenable可以更容易地在widget树中共享状态。任何widget都可以通过ValueListenableBuilder订阅这个值的变化,而无需通过复杂的回调或状态提升机制。
- 解耦UI和状态:ValueListenable和ValueListenableBuilder提供了一种清晰的方式来将UI逻辑与状态管理逻辑分离,使得代码更加模块化和可维护。
结论
尽管直接使用局部变量和setState可以在简单的情况下工作得很好,但对于更复杂的状态管理需求,特别是涉及状态共享和高效更新的情况,使用ValueListenable和ValueListenableBuilder这样的模式可以提供更大的灵活性和效率。这也是为什么在复杂的Flutter应用中,开发者倾向于使用这些更高级的状态管理解决方案。
异步数据Loading(FutureBuilder、StreamBuilder)
很多时候我们会依赖一些异步数据来动态更新UI,比如在打开一个页面时我们需要先从互联网上获取数据,在获取数据的过程中我们显示一个加载框,等获取到数据时我们再渲染页面。通过 StatefulWidget 我们完全可以实现上述这些功能。但由于在实际开发中依赖异步数据更新UI的这种场景非常常见,因此Flutter专门提供了组件来实现功能。
FutureBuilder
FutureBuilder会依赖一个Future,它会根据所依赖的Future的状态来动态构建自身
FutureBuilder({
  this.future,  // `FutureBuilder`依赖的`Future`,通常是一个异步耗时任务。
  this.initialData, // 初始数据,用户设置默认数据。
  required this.builder, // Widget构建器;该构建器会在`Future`执行的不同阶段被多次调用,
})
builder 构建器签名如下:
Function (BuildContext context, AsyncSnapshot snapshot) 
snapshot会包含当前异步任务的状态信息及结果信息 ,比如我们可以通过snapshot.connectionState获取异步任务的状态信息、通过snapshot.hasError判断异步任务是否有错误等等,完整的定义读者可以查看AsyncSnapshot类定义。
另外,FutureBuilder的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();
        }
      },
    ),
  );
}
在builder中根据当前异步任务状态ConnectionState来返回不同的widget。ConnectionState是一个枚举类,定义如下:
enum ConnectionState {
  /// 当前没有异步任务,比如[FutureBuilder]的[future]为null时
  none,
  /// 异步任务处于等待状态
  waiting,
  /// Stream处于激活状态(流上已经有数据传递了),对于FutureBuilder没有该状态。
  active,
  /// 异步任务已经终止.
  done,
}
StreamBuilder
和Future 不同的是,它可以接收多个异步操作的结果,它常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。StreamBuilder正是用于配合Stream来展示流上事件(数据)变化的UI组件:
StreamBuilder({
  this.initialData,
  Stream<T> stream,
  required this.builder,
})