Flutter学习 功能型Widget

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter学习 功能型Widget相关的知识,希望对你有一定的参考价值。

1. WillPopScope

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

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

onWillPop 是回调函数, 当用户点击该按钮时被回调,该函数需要返回一个 Future 对象,如果返回 Future 最终值为 false 时,则当前路由不出栈, 如果最终值为 true 时, 则当前路由出栈

1.1 示例

下面代码是为了防止误触而关闭当前页面的返回键拦截示例, 如果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 内连续按两次返回键退出"),
      ),
    ));
  

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 版本:

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 都去执行这些昂贵的操作。

2.2 深入了解 InheritedWidget

如果我们只想在 _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 的实现方式, 运行示例后,会发现 _TestWidgetStatedidChangeDependencies 不会再被调用,但是build方法会调用,这是因为点击了自增按钮后,会调用 setState,重构整个页面, 而 _TestWidget 并没有做缓存,所以它也会被重建,所以会调用 build 方法

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

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

3. Provider

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

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

3.1 实现简易 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,是一个发布-订阅者模式,通过 addListenerremoveListener 来添加监听者, 用 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 可能会发生变化。

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

3.1.1购物车示例

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

我们来定义一个 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 后带来的好处有:

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

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<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

5. ValueListenableBuilder

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

定义为:

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

5.1 示例

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(
      bodyFlutter-Widget-学习笔记

Flutter学习 Widget简介

Flutter学习-基础Widget

Flutter学习-滚动的Widget

flutter学习-Widget-Element-RenderObject

Flutter学习-多子布局Widget