是导航返回拦截的组件, 类似于 Android 中封装的 onBackPress
方法,来看看它的构造函数:
class WillPopScope extends StatefulWidget {
const WillPopScope({
Key? key,
required this.child,
required this.onWillPop,
})
onWillPop
是回调函数, 当用户点击该按钮时被回调,该函数需要返回一个 Future
对象,如果返回 Future 最终值为 false 时,则当前路由不出栈, 如果最终值为 true 时, 则当前路由出栈
下面代码是为了防止误触而关闭当前页面的返回键拦截示例, 如果1s内两次点击返回按钮,则退出,如果超过1s,则重新计时:
class _WillPopScopeRouteState extends State<WillPopScopeRoute> {
// 上次的点击时间
DateTime? _lastPressedAt;
@override
Widget build(BuildContext context) {
return Scaffold(
body: WillPopScope(
onWillPop: () async {
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt!) >
const Duration(seconds: 1)) {
_lastPressedAt = DateTime.now();
return false;
}
return true;
},
child: Container(
alignment: Alignment.center,
child: Text("1s 内连续按两次返回键退出"),
),
));
}
}
用于数据共享的组件,提供了一种在 Widget 树中从上到下共享数据的方式,比如我们在应用的根 Widget 中通过 InheritedWidget
共享了一个数据,那我们可以在任意子 Widget 树中去获取该共享数据。
这个特性在一些需要整个 Widget 中共享数据的场景中非常方便。比如 Flutter 正是通过该组件来实现 共享应用主题 和 Locale 信息。
在学习 StatefulWidget
时,我们提到了 State
对象有一个 didChangeDependencies
的回调,它会在 “依赖” 发生变化的时候被Flutter框架调用,而这个 “依赖” 就是 子Widget 是否使用了 父Widget 中 InheritedWidget
的数据,如果使用了,则代表 子Widget 有依赖, 如果没有则表示没有这种依赖。
这种机制可以使子组件所依赖的 InheritedWidget
变化时来更新自身, 比如主题、locale 等发生变化时,依赖其 子Widget 的 didChangeDEpendencies
方法就会被调用
下面来看下官方示例中, 计算器应用的 InheritedWidget
版本:
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({Key? key, required this.data, required Widget child})
: super(key: key, child: child);
// 共享数据,代表被点击的次数
final int data;
// 提供一个便捷方法,用于给树中 子Widget 获取共享数据
static ShareDataWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
// 该回调决定了当 data 发生变化的时,是否通知子树中依赖data的widget
@override
bool updateShouldNotify(ShareDataWidget oldWidget) {
return oldWidget.data != data;
}
}
class _TestWidget extends StatefulWidget {
@override
State<_TestWidget> createState() => _TestWidgetState();
}
class _TestWidgetState extends State<_TestWidget> {
@override
Widget build(BuildContext context) {
return Text(ShareDataWidget.of(context)!.data.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 父或祖先Widget 中的 InheritedWidget 发生了改变时被调用
// 如果build中没有依赖,则不会调用该回调
print("依赖发生了改变");
}
}
class InheritedWidgetTestRoute extends StatefulWidget {
@override
_InheritedWidgetTestRouteState createState() =>
_InheritedWidgetTestRouteState();
}
class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> {
int cnt = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ShareDataWidget(
data: cnt,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 25),
child: _TestWidget()),
ElevatedButton(
onPressed: () => setState(() {
++cnt;
}),
child: const Text("自增"))
],
),
),
),
);
}
}
运行后,每当点击自增按钮,打印台就会打印:
如果 _TestWidget 中没有使用 ShareDataWidget 中的数据,那么它的 didChangeDependencies() 将不会调用,因为没有依赖其数据。
didChangeDependencies 中可以做什么?
一般来说, 子 Widget 很少会重写该方法,因为在依赖改变后, Flutter 框架也会调用 build
方法重新构建组件树,但是如果需要在依赖改变后执行一些昂贵的操作,比如数据库存储或者网络库请求,这时最好的方式就是在此方法中执行,这样可以避免每次 build 都去执行这些昂贵的操作。
如果我们只想在 _TestWidgetState
中引用 ShareDataWidget 的数据,却不希望 ShareDataWidget 发生变化时调用了 _TestWidgetState
的方法应该怎么办呢?
我们只需要改一下 ShareDataWidget.of()
的实现方式:
static ShareDataWidget? of(BuildContext context) {
// return context.dependOnInheritedWidgetOfExactType();
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget;
}
唯一的改动是把 dependOnInheritedWidgetOfExactType
方法换成了 getElementForInheritedWidgetOfExactType
,他们有什么区别呢?我们来看下这两个方法的源码:
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
return ancestor;
}
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
if (ancestor != null) {
// 比前者多调用了 dependOnInheritedElement 方法
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
来看下 dependOnInheritedElement
:
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor != null);
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
dependOnInheritedElement
方法主要是注册了依赖关系,加进到一个 HashSet 中。而 getElementForInheritedWidgetOfExactType()
不会。
需要注意的是:上面的示例中如果改成了 getElementForInheritedWidgetOfExactType
的实现方式, 运行示例后,会发现 _TestWidgetState
的 didChangeDependencies
不会再被调用,但是build方法会调用,这是因为点击了自增按钮后,会调用 setState,重构整个页面, 而 _TestWidget
并没有做缓存,所以它也会被重建,所以会调用 build
方法
那么就引入了一个新的问题: 实际上,我们只想更新子树中依赖了 ShareDataWidget
的子节点,而在调用了父组件(这里是 _InheritedWidgetTestRouteState
)setState
方法必然会导致所有子节点build。 这会赵成不必要的浪费,而且可能会出现问题。
而 缓存数据 可以解决这个问题, 就是通过封装一个 StatefulWidget
将 子Widget 树缓存起来,下面就来实现一个 Provider
来演示。
Provider 包的思想是: 将需要跨组件共享的状态保存在 InheritedWidget
中,然后子组件引用 InheritedWidget
, InheritedWidget
会绑定子组件产生依赖关系,然后当数据发生改变时,自动更新子孙组件。
为了加强理解,这里不直接看 Provider 实现,而是实习哪一个最小功能的 Provider
这里引入泛型 ,便于外界能够保存更通用的数据
class InheritedProvider<T> extends InheritedWidget {
InheritedProvider({required this.data, required Widget child})
: super(child: child);
final T data;
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
// 这里先返回true
return true;
}
第二步,我们来实现 “数据发生改变时该如何改变?”, 这里的做法是通过使用加监听器, Flutter 中有 ChangeNotifier
,继承自 Listenable
,是一个发布-订阅者模式,通过 addListener
、 removeListener
来添加监听者, 用 notifyListener
来触发监听器的回调。
所以我们将共享的状态放到一个 Model 类中,然后让它继承自 ChangeNotifier
, 这样当共享状态改变时,只需要调用 notify 就可以通知订阅者,订阅者来重新构建 InheritedProvider 了:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
ChangeNotifierProvider({Key? key, required this.data, required this.child});
final Widget child;
final T data;
static T of<T>(BuildContext context) {
final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
return provider!.data;
}
@override
State<StatefulWidget> createState() => _ChangeNotifierProviderState<T>();
}
该类继承自 StatefulWidget
,然后提供 of 方法供子类方便获取 Widget 树中的 InheritedProvider
中保存的共享状态, 下面来实现该类对应的 State 类:
class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider> {
void update() {
setState(() {
// 如果数据发生了变化,则重新构建 InheritedProvider
});
}
@override
void didUpdateWidget(
covariant ChangeNotifierProvider<ChangeNotifier> oldWidget) {
if (widget.data != oldWidget.data) {
// 当 Provider 更新时,如果新旧数据不相同,则解绑截数据监听,同时添加新数据监听
oldWidget.data.removeListener(update);
widget.data.addListener(update);
}
super.didUpdateWidget(oldWidget);
}
@override
void initState() {
// 给 model 添加监听器
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
widget.data.removeListener(update);
super.dispose();
}
@override
Widget build(BuildContext context) {
return InheritedProvider(
data: widget.data,
child: widget.child,
);
}
}
可以看到, _ChangeNotifierProviderState
类的主要作用是监听到共享状态改变时,重新构建 Widget 树。在 _ChangeNotiferProviderState
中调用 setState 方法, widget.child
始终是同一个,所以执行 build 时, InheritedProvider
的child引用的始终是同一个 子widget, 所以 widget.child
并不会重新 build , 这也就相当于对 child 进行了缓存,当然如果 ChangeNotifierProvider 的 父Widget 重新build 时, 则其传入的 child 可能会发生变化。
接下来我们用该组件实现一个 购物车示例。
我们需要实现一个显示购物车中所有商品总价的功能,而这个价格显然就是我们想要共享的状态, 因为购物车的价格会随着商品的添加和移除而改变。
我们来定义一个 Item
类,用于表示商品信息:
class Item {
Item(this.price, this.count);
// 商品单价
double price;
// 商品数量
int count;
}
接着定义一个保存购物车内商品数据的 CartModel
类:
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
// 禁止改变购物车里面的信息
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
// 总价
double get totalPrice => _items.fold(
0,
(previousValue, element) =>
previousValue + element.count * element.price);
// 将 [item] 添加到购物车, 该方法的作用是外部改变购物车
void add(Item item) {
_items.add(item);
// 通知订阅者重新构建 InheritedProvider 来更新状态
notifyListeners();
}
}
这个 CartModel 就是我们需要跨组件共享的数据类型,最后我们写一个示例页面:
class _ProviderRouteState extends State<ProviderRoute> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ChangeNotifierProvider<CartModel>(
data: CartModel(),
child: Builder(builder: (context) {
return Column(
children: [
Builder(builder: (context) {
var cart = ChangeNotifierProvider.of<CartModel>(context);
return Text("总价为:${cart?.totalPrice}");
}),
Builder(builder: (context) {
print("ElevatedButton build");
return ElevatedButton(
onPressed: () {
ChangeNotifierProvider.of<CartModel>(context)
?.add(Item(15, 1));
},
child: Text("添加商品"));
})
],
);
}),
),
),
);
}
}
接下来每次点击添加商品的按钮都会增加15块钱。一般来说, ChangeNotifierProvider
作为整个 App 的路由优势会非常明显,可以共享数据到整个App中去。Provider 的模型如下图所示:
使用 Provider 后带来的好处有:
ThemeData
用于保存 Material 组件库中的主题数据, 它包含了可以自定义的部分,我们可以通过 ThemeData 来自定义应用主题,在子组件中,我们可以通过 Theme.of
方法来获取当前的 ThemeData。
ThemeData 的可定义属性非常之多,下面截取一些常用的构造属性:
ThemeData({
Brightness? brightness, //深色还是浅色
MaterialColor? primarySwatch, //主题颜色样本,见下面介绍
Color? primaryColor, //主色,决定导航栏颜色
Color? cardColor, //卡片颜色
Color? dividerColor, //分割线颜色
ButtonThemeData buttonTheme, //按钮主题
Color dialogBackgroundColor,//对话框背景颜色
String fontFamily, //文字字体
TextTheme textTheme,// 字体主题,包括标题、body等文字样式
IconThemeData iconTheme, // Icon的默认样式
TargetPlatform platform, //指定平台,应用特定平台控件风格
ColorScheme? colorScheme,
...
})
下面我们来实现一个路由换肤的功能:
class _ThemeRouteState extends State<ThemeRoute> {
// 当前主题颜色
MaterialColor _themeColor = Colors.teal;
@override
Widget build(BuildContext context) {
ThemeData themeData = Theme.of(context);
return Theme(
data: ThemeData(
// 用于导航栏、 FloatingActionButton 的颜色
primarySwatch: _themeColor,
// 用于 Icon 的颜色
iconTheme: IconThemeData(color: _themeColor)),
child: Scaffold(
appBar: AppBar(title: Text("主题测试")),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 第一行 Icon 使用主题中的 iconTheme
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite),
Icon(Icons.airport_shuttle),
Text("颜色跟随主题")
],
),
// 第二行 Icon 自定义颜色
Theme(
data: themeData.copyWith(
iconTheme: themeData.iconTheme.copyWith(color: Colors.blue)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite),
Icon(Icons.airport_shuttle),
Text("颜色固定蓝色")
],
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() => _themeColor =
_themeColor == Colors.teal ? Colors.green : Colors.teal),
child: Icon(Icons.palette),
),
),
);
}
}
效果如下:
我们可以通过局部主题覆盖全局主题,如果需要对整个应用换肤,可以修改 MaterialApp
的 theme
InheritedWidget
提供了一种从上到下的数据共享方式,而有些场景并非从上到下传递,比如横向传递或者从下到上,为了解决这个问题, Flutter 提供了 ValueListenableBuilder
组件,它的功能是监听一个数据源,如果数据源发生了变化,则会重新执行其 builder。
定义为:
const ValueListenableBuilder({
Key? key,
required this.valueListenable,
required this.builder,
this.child,
})
valueListenable
ValueListenable
builder
child
class _ValueListenableState extends State<ValueListenableRoute> {
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
static const double textScaleFactor = 1.5;
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: ValueListenableBuilder<int>(
builder: (context, value, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
child!,
Text("$value times", textScaleFactor: textScaleFactor)
],
);
},
child: const Text("click", textScaleFactor: textScaleFactor),
valueListenable: _counter,
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => _counter.value++,
),
);
}
}
这是一个计数器的demo。 在打开页面时执行了一次build方法,在点击 + 号时,真个页面并没有重新build, 只是 VlaueListenableBuilder
重新构建了组件树。
因此使用建议是: 尽可能让 ValueListenableBuilder 只构建依赖数据源的 Widget, 这样可以缩小构建范围,也就是说 ValueListenableBuilder 的拆分粒度可以更细
很多时候我们会依赖一些异步数据来动态更新 UI,比如我们要先获取Http数据,然后获取数据过程中显示一个加载框,等获取到数据时我们再渲染页面,又比如想展示 Stream 的进度。 Flutter 则分别提供了 FutureBuilder
和 StreamBuilder
两个组件来快速实现这两个功能,示例比较简单,这里就不再列举了