跳到主要内容

七、数据共享

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

注意:

  1. 首先,定义一个扩展自 InheritedWidget 的类,该类将持有要共享的数据。
  2. 在页面中使用 MyInheritedWidget 并向其提供共享数据
  3. 在任何子(后代)widget中,都可以通过调用 MyInheritedWidget.of(context) 来获取共享的数据

数据更新钩子(didChangeDependencies)

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

当 widget 第一次构建时,didChangeDependencies 会在 initState 之后立即被调用: 此外,每当该 widget 的父级或祖先 widget 中的 InheritedWidget 发生变化时,didChangeDependencies 也会被调用。 注意: 4. 如果build中没有依赖MyInheritedWidget中的数据,didChangeDependencies不会被调用 5. MyInheritedWidgetupdateShouldNotify, 如果返回true则会触发后代组件didChangeDependencies钩子执行,固定返回false也不影响build的界面变化

使用场景

一般来说,子 widget 很少会重写此方法,因为在依赖改变后 Flutter 框架也都会调用build()方法重新构建组件树

  • 如果你需要在依赖改变后执行一些昂贵的操作,比如网络请求,这时最好的方式就是在此方法中执行,这样可以避免每次build()都执行这些昂贵操作。
  • 订阅 InheritedWidget: 如果 widget 依赖于 InheritedWidget 中的数据,可以在 didChangeDependencies 中订阅这些数据。当数据发生变化时,didChangeDependencies 会被调用,在这里更新 widget 以响应这些变化。
  • 读取上下文依赖的数据: 有些类,如 ThemeMediaQuery,也是通过 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');
}
}

自己实现一个

  1. 定义 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();
  1. 定义事件
class UserLoggedInEvent {
final String username;

UserLoggedInEvent(this.username);
}

class UserLoggedOutEvent {}
  1. 订阅和发布事件
void main() {
// 订阅登录事件
eventBus.on<UserLoggedInEvent>((event) {
print('User logged in: ${event.username}');
});

// 模拟用户登录
eventBus.fire(UserLoggedInEvent('Alice'));
}
  1. 取消订阅
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();
}
}

对比直接使用局部变量的优势

局部变量的局限性

  1. 局部变量不会触发重建:当局部变量的值改变时,它不会自动触发widget的重建。你需要手动调用setState来通知框架状态已经改变,从而导致widget重建。
  2. 状态共享的复杂性:如果你需要将局部变量的状态在widget树的不同部分之间共享,这会变得比较复杂。你可能需要将状态提升到共同的祖先widget中,然后通过回调函数或其他方式将状态传递给需要它的子widget。
  3. 状态更新的效率问题:使用setState更新局部变量会导致当前widget及其所有子widget的重建,这在复杂的widget树中可能会影响性能。

使用ValueListenableValueListenableBuilder的优势

  1. 细粒度的重建控制ValueListenableBuilder仅在ValueListenable对象的值发生变化时重建其子树。这意味着只有真正依赖于这个值的widget会被重建,从而提高了效率。
  2. 简化的状态共享:使用ValueListenable可以更容易地在widget树中共享状态。任何widget都可以通过ValueListenableBuilder订阅这个值的变化,而无需通过复杂的回调或状态提升机制。
  3. 解耦UI和状态ValueListenableValueListenableBuilder提供了一种清晰的方式来将UI逻辑与状态管理逻辑分离,使得代码更加模块化和可维护。

结论

尽管直接使用局部变量和setState可以在简单的情况下工作得很好,但对于更复杂的状态管理需求,特别是涉及状态共享和高效更新的情况,使用ValueListenableValueListenableBuilder这样的模式可以提供更大的灵活性和效率。这也是为什么在复杂的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类定义。

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

示例

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

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