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

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();
    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 的模型如下图所示:
Flutter学习 功能型Widget_第1张图片
使用 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),
        ),
      ),
    );
  }
}

效果如下:
Flutter学习 功能型Widget_第2张图片
Flutter学习 功能型Widget_第3张图片
我们可以通过局部主题覆盖全局主题,如果需要对整个应用换肤,可以修改 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<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 的拆分粒度可以更细

6. 异步 UI 更新

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

你可能感兴趣的:(Flutter,flutter)