赞
踩
是导航返回拦截的组件, 类似于 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<ShareDataWidget>();
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<T>
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
两个组件来快速实现这两个功能,示例比较简单,这里就不再列举了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。