Flutter学习 功能型Widget

网友投稿 243 2022-11-16

Flutter学习 功能型Widget

文章目录

​​1. WillPopScope​​

​​1.1 示例​​

​​2. InheritedWidget​​

​​2.1 didChangeDependencies​​​​2.2 深入了解 InheritedWidget​​

​​3. Provider​​

​​3.1 实现简易 Provider​​

​​3.1.1购物车示例​​

​​4. 主题 Theme​​​​5. ValueListenableBuilder​​

​​5.1 示例​​

​​6. 异步 UI 更新​​

1. WillPopScope

是导航返回拦截的组件, 类似于 Android 中封装的 ​​onBackPress​​ 方法,来看看它的构造函数:

class WillPopScope extends StatefulWidget { const WillPopScope({ Key? key, required this.child, required this.onWillPop, })

1.1 示例

2. InheritedWidget

用于数据共享的组件,提供了一种在 Widget 树中从上到下共享数据的方式,比如我们在应用的根 Widget 中通过 ​​InheritedWidget​​ 共享了一个数据,那我们可以在任意子 Widget 树中去获取该共享数据。

这个特性在一些需要整个 Widget 中共享数据的场景中非常方便。比如 Flutter 正是通过该组件来实现 共享应用主题 和 Locale 信息。

2.1 didChangeDependencies

在学习 ​​StatefulWidget​​​ 时,我们提到了 ​​State​​​ 对象有一个 ​​didChangeDependencies​​​ 的回调,它会在 “依赖” 发生变化的时候被Flutter框架调用,而这个 “依赖” 就是 子Widget 是否使用了 父Widget 中 ​​InheritedWidget​​ 的数据,如果使用了,则代表 子Widget 有依赖, 如果没有则表示没有这种依赖。

这种机制可以使子组件所依赖的 ​​InheritedWidget​​​ 变化时来更新自身, 比如主题、locale 等发生变化时,依赖其 子Widget 的 ​​didChangeDEpendencies​​ 方法就会被调用

下面来看下官方示例中, 计算器应用的 ​​InheritedWidget​​ 版本:

如果 _TestWidget 中没有使用 ShareDataWidget 中的数据,那么它的 didChangeDependencies() 将不会调用,因为没有依赖其数据。

didChangeDependencies 中可以做什么? 一般来说, 子 Widget 很少会重写该方法,因为在依赖改变后, Flutter 框架也会调用 ​​​build​​ 方法重新构建组件树,但是如果需要在依赖改变后执行一些昂贵的操作,比如数据库存储或者网络库请求,这时最好的方式就是在此方法中执行,这样可以避免每次 build 都去执行这些昂贵的操作。

2.2 深入了解 InheritedWidget

如果我们只想在 ​​_TestWidgetState​​​ 中引用 ShareDataWidget 的数据,却不希望 ShareDataWidget 发生变化时调用了 ​​_TestWidgetState​​ 的方法应该怎么办呢?

我们只需要改一下 ​​ShareDataWidget.of()​​ 的实现方式:

? of(BuildContext context) { // return context.dependOnInheritedWidgetOfExactType(); return context.getElementForInheritedWidgetOfExactType()!.widget as ShareDataWidget; }

唯一的改动是把 ​​dependOnInheritedWidgetOfExactType​​​ 方法换成了 ​​getElementForInheritedWidgetOfExactType​​ ,他们有什么区别呢?我们来看下这两个方法的源码:

override InheritedElement? getElementForInheritedWidgetOfExactType() { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T]; return ancestor; } @override T? dependOnInheritedWidgetOfExactType({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(); _dependencies!.add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget; }

​​dependOnInheritedElement​​​ 方法主要是注册了依赖关系,加进到一个 HashSet 中。而 ​​getElementForInheritedWidgetOfExactType()​​ 不会。

那么就引入了一个新的问题: 实际上,我们只想更新子树中依赖了 ​​ShareDataWidget​​​ 的子节点,而在调用了父组件(这里是 ​​_InheritedWidgetTestRouteState​​​)​​setState​​ 方法必然会导致所有子节点build。 这会赵成不必要的浪费,而且可能会出现问题。

而 缓存数据 可以解决这个问题, 就是通过封装一个 ​​StatefulWidget​​​ 将 子Widget 树缓存起来,下面就来实现一个 ​​Provider​​ 来演示。

3. Provider

Provider 包的思想是: 将需要跨组件共享的状态保存在 ​​InheritedWidget​​​ 中,然后子组件引用 ​​InheritedWidget​​​ , ​​InheritedWidget​​ 会绑定子组件产生依赖关系,然后当数据发生改变时,自动更新子孙组件。

为了加强理解,这里不直接看 Provider 实现,而是实习哪一个最小功能的 Provider

3.1 实现简易 Provider

这里引入泛型 ,便于外界能够保存更通用的数据

class InheritedProvider 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 extends StatefulWidget { ChangeNotifierProvider({Key? key, required this.data, required this.child}); final Widget child; final T data; static T of(BuildContext context) { final provider = context.dependOnInheritedWidgetOfExactType>(); return provider!.data; } @override State createState() => _ChangeNotifierProviderState();}

该类继承自 ​​StatefulWidget​​​,然后提供 of 方法供子类方便获取 Widget 树中的 ​​InheritedProvider​​ 中保存的共享状态, 下面来实现该类对应的 State 类:

class _ChangeNotifierProviderState extends State { void update() { setState(() { // 如果数据发生了变化,则重新构建 InheritedProvider }); } @override void didUpdateWidget( covariant ChangeNotifierProvider 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 可能会发生变化。

接下来我们用该组件实现一个 购物车示例。

3.1.1购物车示例

我们需要实现一个显示购物车中所有商品总价的功能,而这个价格显然就是我们想要共享的状态, 因为购物车的价格会随着商品的添加和移除而改变。

我们来定义一个 ​​Item​​ 类,用于表示商品信息:

class Item { Item(this.price, this.count); // 商品单价 double price; // 商品数量 int count; }

接着定义一个保存购物车内商品数据的 ​​CartModel​​ 类:

class CartModel extends ChangeNotifier { final List _items = []; // 禁止改变购物车里面的信息 UnmodifiableListView 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 { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ChangeNotifierProvider( data: CartModel(), child: Builder(builder: (context) { return Column( children: [ Builder(builder: (context) { var cart = ChangeNotifierProvider.of(context); return Text("总价为:${cart?.totalPrice}"); }), Builder(builder: (context) { print("ElevatedButton build"); return ElevatedButton( onPressed: () { ChangeNotifierProvider.of(context) ?.add(Item(15, 1)); }, child: Text("添加商品")); }) ], ); }), ), ), ); }}

使用 Provider 后带来的好处有:

业务代码值关心数据更新,只需要更新 Model, UI就会自动更新,而不用在状态改变后去手动调用 setState 来显示刷新页面数据改变的消息传递被屏蔽了大型复杂场景下,使用全局共享变量会简化代码逻辑

4. 主题 Theme

​​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 { // 当前主题颜色 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

5. ValueListenableBuilder

​​InheritedWidget​​​ 提供了一种从上到下的数据共享方式,而有些场景并非从上到下传递,比如横向传递或者从下到上,为了解决这个问题, Flutter 提供了 ​​ValueListenableBuilder​​ 组件,它的功能是监听一个数据源,如果数据源发生了变化,则会重新执行其 builder。

定义为:

const ValueListenableBuilder({ Key? key, required this.valueListenable, required this.builder, this.child, })

​​valueListenable​​​ 表示一个可监听的数据源, 类型为 ​​ValueListenable​​​​builder​​ 数据源发生变化通知时, 会重新调用 builder 重新 build 子组件树​​child​​ builder 中每次都会重新构建子组件树,如果子组件树中有一些不变的部分,可以将其传递给 child, child 会作为 builder 的第三个参数传递给 builder, 通过这种方式就可以实现组件缓存

5.1 示例

class _ValueListenableState extends State { final ValueNotifier _counter = ValueNotifier(0); static const double textScaleFactor = 1.5; @override Widget build(BuildContext context) { print("build"); return Scaffold( body: Center( child: ValueListenableBuilder( builder: (context, value, child) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ child!, Text("$value, textScaleFactor: textScaleFactor) ], ); }, child: const Text("click", textScaleFactor: textScaleFactor), valueListenable: _counter, ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () => _counter.value++, ), ); }}

因此使用建议是: 尽可能让 ValueListenableBuilder 只构建依赖数据源的 Widget, 这样可以缩小构建范围,也就是说 ValueListenableBuilder 的拆分粒度可以更细

6. 异步 UI 更新

很多时候我们会依赖一些异步数据来动态更新 UI,比如我们要先获取Http数据,然后获取数据过程中显示一个加载框,等获取到数据时我们再渲染页面,又比如想展示 Stream 的进度。 Flutter 则分别提供了 ​​FutureBuilder​​​ 和 ​​StreamBuilder​​ 两个组件来快速实现这两个功能,示例比较简单,这里就不再列举了

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Java中的synchronized关键字
下一篇:测试测量技术如何保证O-RAN网络的成功部署
相关文章

 发表评论

暂时没有评论,来抢沙发吧~