美文网首页
Flutter 第三方库之 Scoped_model

Flutter 第三方库之 Scoped_model

作者: seraphzxz | 来源:发表于2020-12-23 15:46 被阅读0次

    一、Scoped_model 简介

    A set of utilities that allow you to easily pass a data Model from a parent Widget down to its descendants. In addition, it also rebuilds all of the children that use the model when the model is updated. This library was originally extracted from the Fuchsia codebase.

    Scoped_model 是一个 dart 第三方库,提供了让开发者能够轻松地将数据模型从父 Widget 传递到它的后代的功能。此外,它还会在模型更新时重新渲染使用该模型的所有子项。

    它直接来自于 Google 正在开发的新系统 Fuchsia 中的核心 Widgets 中对 Model 类的简单提取,作为独立使用的独立 Flutter 插件发布。

    Scoped_model 提供三个主要的类:

    1. Model 类:开发者创建的 Model 需要继承该类,并可以监听 Models 的变化。

    2. ScopedModel 类:如果想要将 Model 下发到 Widget hierarchy,可以使用 ScopedModel Widget 对 Model 进行包裹。这会使得该 Widget的所有子孙节点可以使用该被包裹的 Model。

    3. ScopedModelDescendant 类:开发者可以使用该类在 Widget 树中寻找合适的 ScopedModel 。它还会在模型更新时重新渲染使用该模型的所有子项。

    Scoped_model 基于 Flutter 的多种特性创建,包括:

    • Model 实现了 Listenable 接口

      • AnimationControllerTextEditingController同样实现了Listenable
    • Model 使用InheritedWidget进行传递。当一个InheritedWidget重建后,它将精准地重建所有以来该数据的 Widget。不需要管理订阅。

    • Scoped_model 使用AnimatedBuilder,当 model 发生变化时 Widget 通过高级选项来监听 Model 和 InheritedWidget的重建

    二、示例

    官方实例代码:

    import 'package:flutter/material.dart';
    import 'package:scoped_model/scoped_model.dart';
    
    void main() {
     runApp(MyApp(
       model: CounterModel(),
     ));
    }
    
    class MyApp extends StatelessWidget {
     final CounterModel model;
    
     const MyApp({Key key, @required this.model}) : super(key: key);
    
     @override
     Widget build(BuildContext context) {
       // At the top level of our app, we'll, create a ScopedModel Widget. This
       // will provide the CounterModel to all children in the app that request it
       // using a ScopedModelDescendant.
       return ScopedModel<CounterModel>(
         model: model,
         child: MaterialApp(
           title: 'Scoped Model Demo',
           home: CounterHome('Scoped Model Demo'),
         ),
       );
     }
    }
    
    // Start by creating a class that has a counter and a method to increment it.
    //
    // Note: It must extend from Model.
    class CounterModel extends Model {
     int _counter = 0;
    
     int get counter => _counter;
    
     void increment() {
       // First, increment the counter
       _counter++;
    
       // Then notify all the listeners.
       notifyListeners();
     }
    }
    
    class CounterHome extends StatelessWidget {
     final String title;
    
     CounterHome(this.title);
    
     @override
     Widget build(BuildContext context) {
       return Scaffold(
         appBar: AppBar(
           title: Text(title),
         ),
         body: Center(
           child: Column(
             mainAxisAlignment: MainAxisAlignment.center,
             children: <Widget>[
               Text('You have pushed the button this many times:'),
               // Create a ScopedModelDescendant. This widget will get the
               // CounterModel from the nearest parent ScopedModel<CounterModel>.
               // It will hand that CounterModel to our builder method, and
               // rebuild any time the CounterModel changes (i.e. after we
               // `notifyListeners` in the Model).
               ScopedModelDescendant<CounterModel>(
                 builder: (context, child, model) {
                   return Text(
                     model.counter.toString(),
                     style: Theme.of(context).textTheme.headline4,
                   );
                 },
               ),
             ],
           ),
         ),
         // Use the ScopedModelDescendant again in order to use the increment
         // method from the CounterModel
         floatingActionButton: ScopedModelDescendant<CounterModel>(
           builder: (context, child, model) {
             return FloatingActionButton(
               onPressed: model.increment,
               tooltip: 'Increment',
               child: Icon(Icons.add),
             );
           },
         ),
       );
     }
    }
    

    三、实现原理

    Scoped model使用了观察者模式,将数据模型放在父代,后代通过找到父代的model进行数据渲染,最后数据改变时将数据传回,父代再通知所有用到了该model的子代去更新状态。
    该框架实际上是利用InheritedWidget实现数据的传递的。在下一小节中会对InheritedWidget及其原理进行分析,读者可以先阅读下一节进行了解。

    3.1 源码分析

    下面来分析 Scoped_model 的源码和实现。

    Model

    abstract class Model extends Listenable {
      final Set<VoidCallback> _listeners = Set<VoidCallback>();
      int _version = 0;
      int _microtaskVersion = 0;
    
      /// [listener] will be invoked when the model changes.
      @override
      void addListener(VoidCallback listener) {
        _listeners.add(listener);
      }
    
      /// [listener] will no longer be invoked when the model changes.
      @override
      void removeListener(VoidCallback listener) {
        _listeners.remove(listener);
      }
    
      /// Returns the number of listeners listening to this model.
      int get listenerCount => _listeners.length;
    
      /// Should be called only by [Model] when the model has changed.
      @protected
      void notifyListeners() {
        // We schedule a microtask to debounce multiple changes that can occur
        // all at once.
        if (_microtaskVersion == _version) {
          _microtaskVersion++;
          // Schedules a callback to be called before all other currently scheduled ones.
          scheduleMicrotask(() {
            _version++;
            _microtaskVersion = _version;
            _listeners.toList().forEach((VoidCallback listener) => listener());
          });
        }
      }
    }
    

    Model类很简单,就是一个实现了Listenable的抽象类,这里的核心方法就是notifyListeners的实现,这里使用了“微任务”来实现防抖动。

    ScopedModel 构造函数

    class ScopedModel<T extends Model> extends StatelessWidget {
      /// The [Model] to provide to [child] and its descendants.
      final T model;
    
      /// The [Widget] the [model] will be available to.
      final Widget child;
    
      ScopedModel({@required this.model, @required this.child})
          : assert(model != null),
            assert(child != null);
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: model,
          builder: (context, _) => _InheritedModel<T>(model: model, child: child),
        );
      }
    
      // 向外暴露方式
      static T of<T extends Model>(
        BuildContext context, {
        bool rebuildOnChange = false,
      }) {
        Widget widget = rebuildOnChange
            ? context.dependOnInheritedWidgetOfExactType<_InheritedModel<T>>()
            : context
                .getElementForInheritedWidgetOfExactType<_InheritedModel<T>>()
                ?.widget;
    
        if (widget == null) {
          throw ScopedModelError();
        } else {
          return (widget as _InheritedModel<T>).model;
        }
      }
    }
    

    首先看一下of()方法,该方法是用来向外/子类来暴露当前对象的,根据 rebuildOnChange 有两种返回方式:

    1. dependOnInheritedWidgetOfExactType<_InheritedModel<T>>():该方法会注册依赖关系。

    2. getElementForInheritedWidgetOfExactType<_InheritedModel<T>>():该方法不会注册依赖关系。

    两个方法的源码如下:

    @override
    InheritedElement getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
      assert(_debugCheckStateIsActiveForAncestorLookup());
      final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
      return ancestor;
    }
    @override
    InheritedWidget dependOnInheritedWidgetOfExactType({ Object aspect }) {
      assert(_debugCheckStateIsActiveForAncestorLookup());
      final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[T];
      //多出的部分
      if (ancestor != null) {
        assert(ancestor is InheritedElement);
        return dependOnInheritedElement(ancestor, aspect: aspect) as T;
      }
      _hadUnsatisfiedDependencies = true;
      return null;
    }
    

    我们可以看到,dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()多调了dependOnInheritedElement方法,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方法中主要是注册了依赖关系,也就是说,调用dependOnInheritedWidgetOfExactType()getElementForInheritedWidgetOfExactType()的区别就是前者会注册依赖关系,而后者不会,所以在调用dependOnInheritedWidgetOfExactType()时,InheritedWidget和依赖它的子孙组件关系便完成了注册,之后当InheritedWidget发生变化时,就会更新依赖它的子孙组件,也就是会调这些子孙组件的didChangeDependencies()方法和build()方法。而当调用的是 getElementForInheritedWidgetOfExactType()时,由于没有注册依赖关系,所以之后当InheritedWidget发生变化时,就不会更新相应的子孙Widget。文章的后面会专门对InheritedWidget进行分析。

    再来看build的实现,该方法返回了可以处理动画的AnimatedBuilder,其中 builder 类型为 _InheritedModel,该类源码如下:

    class _InheritedModel<T extends Model> extends InheritedWidget {
      final T model;
      final int version;
    
      _InheritedModel({Key key, Widget child, T model})
          : this.model = model,
            this.version = model._version,
            super(key: key, child: child);
    
      // 该回调决定当 data 发生变化时,是否通知子树中依赖 data 的 Widget 
      @override
      bool updateShouldNotify(_InheritedModel<T> oldWidget) =>
          (oldWidget.version != version);
    }
    

    代码很简单,就是通过实现updateShouldNotify来否通知子树中依赖 data 的 Widget。

    ScopedModelDescendant

    /// Builds a child for a [ScopedModelDescendant].
    typedef Widget ScopedModelDescendantBuilder<T extends Model>(
      BuildContext context,
      Widget child,
      T model,
    );
    
    class ScopedModelDescendant<T extends Model> extends StatelessWidget {
      /// Builds a Widget when the Widget is first created and whenever
      /// the [Model] changes if [rebuildOnChange] is set to `true`.
      final ScopedModelDescendantBuilder<T> builder;
    
      /// An optional constant child that does not depend on the model.  This will
      /// be passed as the child of [builder].
      final Widget child;
    
      /// An optional value that determines whether the Widget will rebuild when
      /// the model changes.
      final bool rebuildOnChange;
    
      /// Creates the ScopedModelDescendant
      ScopedModelDescendant({
        @required this.builder,
        this.child,
        this.rebuildOnChange = true,
      });
    
      @override
      Widget build(BuildContext context) {
        return builder(
          context,
          child,
          ScopedModel.of<T>(context, rebuildOnChange: rebuildOnChange),
        );
      }
    }
    

    开发者可以使用该类在 Widget 树中寻找合适的 ScopedModel,其实是通过 ScopedModelof()方法来获取合适的 InheritedWidget

    到这里 Scoped_model 源码和实现就分析完了,其实就是利用InheritedWidget来实现数据的层层下发,下面就来分析该类。

    3.2 InheritedWidget 原理分析

    InheritedWidget是Flutter中非常重要的一个功能型组件,它提供了一种数据在 widget 树中从上到下传递、共享的方式,比如我们在应用的根widget中通过InheritedWidget共享了一个数据,那么我们便可以在任意子 widget 中来获取该共享的数据。

    这个特性在一些需要在 widge t树中共享数据的场景中非常方便,如 Flutter SDK 中正是通过 InheritedWidget 来共享应用主题(Theme)和 Locale (当前语言环境)信息的。

    3.2.1 InheritedWidget 的使用方法

    先看一个InheritedWidget最简单的使用示例:

    import 'package:flutter/material.dart';
    
    void main() => runApp(new MyApp());
    
    class MyWelcomeInfo extends InheritedWidget {
      MyWelcomeInfo({Key key, this.welcomeInfo, Widget child})
          : super(key: key, child: child);
    
      final String welcomeInfo;
    
      // 该回调决定当 data 发生变化时,是否通知子树中依赖 data 的 Widget  
      @override
      bool updateShouldNotify(InheritedWidget oldWidget) {
        return oldWidget.welcomeInfo != welcomeInfo;
      }
    }
    
    class MyNestedChild extends StatelessWidget {
      @override
      build(BuildContext context) {
        // 通过 inheritFromWidgetOfExactType 获取 InheritedWidget
        final MyWelcomeInfo widget =
            context.inheritFromWidgetOfExactType(MyWelcomeInfo);
        return Text(widget.welcomeInfo);
      }
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return new MaterialApp(
          title: 'Flutter InheritWidget',
          home: MyWelcomeInfo(
              welcomeInfo: 'hello flutter',
              child: Center(
                child: MyNestedChild(),
              )),
        );
      }
    }
    

    可以看出我们使用InheritedWidget时涉及到的工作量主要有 2 部分:

    • 创建一个继承自InheritedWidget的类,并将其插入 Widget 树

    • 通过 BuildContext 对象提供的 inheritFromWidgetOfExactType 方法查找 Widget 树中最近的一个特定类型的 InheritedWidget 类的实例

    这里还暗含了一个逻辑,那就是当通过 inheritFromWidgetOfExactType 查找特定类型InheritedWidget时,InheritedWidget的信息是由父元素层层向子元素传递下来?还是 inheritFromWidgetOfExactType 方法自己层层向上查找呢?

    接下来让我们从源码的角度分别看看 Flutter 框架对以上几部分的实现。

    3.2.2 原理分析

    InheritedWidget 源码如下:

    abstract class InheritedWidget extends ProxyWidget {
      /// Abstract const constructor. This constructor enables subclasses to provide
      /// const constructors so that they can be used in const expressions.
      const InheritedWidget({ Key key, Widget child })
        : super(key: key, child: child);
    
      @override
      InheritedElement createElement() => InheritedElement(this);
    
      /// Whether the framework should notify widgets that inherit from this widget.
      ///
      /// When this widget is rebuilt, sometimes we need to rebuild the widgets that
      /// inherit from this widget but sometimes we do not. For example, if the data
      /// held by this widget is the same as the data held by `oldWidget`, then we
      /// do not need to rebuild the widgets that inherited the data held by
      /// `oldWidget`.
      ///
      /// The framework distinguishes these cases by calling this function with the
      /// widget that previously occupied this location in the tree as an argument.
      /// The given widget is guaranteed to have the same [runtimeType] as this
      /// object.
      @protected
      bool updateShouldNotify(covariant InheritedWidget oldWidget);
    }
    

    它是一个继承自 ProxyWidget 的抽象类。内部没什么逻辑,除了实现了一个 createElement 方法之外,还定义了一个 updateShouldNotify()接口。 每次当InheritedElement的实例更新时会执行该方法并传入更新之前对应的 Widget 对象,如果该方法返回 true 那么依赖该 Widget 的(在 build 阶段通过 inheritFromWidgetOfExactType 方法查找过该 Widget 的子 widget)实例会被通知进行更新;如果返回 false 则不会通知依赖项更新。

    3.2.3 InheritedWidget 相关信息的传递机制

    每个 Element 实例上都有一个 _inheritedWidgets 属性。该属性的类型为:

    Map<Type, InheritedElement> _inheritedWidgets;
    

    其中保存了祖先节点中出现的InheritedWidget与其对应 element 的映射关系。在 element 的 mount 阶段active 阶段,会执行 _updateInheritance() 方法更新这个映射关系

    普通 Element 实例

    对于普通 Element 实例,_updateInheritance() 只是单纯把父 element 的 _inheritedWidgets 属性保存在自身 _inheritedWidgets从而实现映射关系的层层向下传递

      void _updateInheritance() {
        assert(_active);
        _inheritedWidgets = _parent?._inheritedWidgets;
      }
    

    InheritedElement

    InheritedWidget创建的InheritedElement 重写了该方法

      void _updateInheritance() {
        assert(_active);
        final Map<Type, InheritedElement> incomingWidgets = _parent?._inheritedWidgets;
        if (incomingWidgets != null)
          _inheritedWidgets = new HashMap<Type, InheritedElement>.from(incomingWidgets);
        else
          _inheritedWidgets = new HashMap<Type, InheritedElement>();
        _inheritedWidgets[widget.runtimeType] = this;
      }
    

    可以看出 InheritedElement 实例会把自身的信息添加到 _inheritedWidgets 属性中,这样其子孙 element 就可以通过前面提到的 _inheritedWidgets 的传递机制获取到此 InheritedElement 的引用。

    3.2.4 InheritedWidget 的更新通知机制

    从前问可知_inheritedWidgets属性存在于 Element 实例上,而代码中调用的 inheritFromWidgetOfExactType方法则存在于 BuildContext实例之上。那么BuildContext是如何获取 Element 实例上的信息的呢?答案是不需要获取。因为每一个 Element 实例也都是一个 BuildContext 实例。这一点可以从 Element 的定义中得到:

    abstract  class  Element  extends  DiagnosticableTree  implements  BuildContext {
      ...
    }
    

    而每次 Element 实例执行 Widget 实例的 build 方法时传入的 context 就是该 Element 实例自身,以 StatelessElement 为例:

    class StatelessElement extends ComponentElement {
      ...
      @override
      Widget build() => widget.build(this);
      ...
    }
    

    既然可以拿到 InheritedWidget 的信息了,接下来通过源码看看更新通知机制的具体实现。

    首先看一下 inheritFromWidgetOfExactType 的实现

      @override
      InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
        ...
        // 获取实例
        final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
        if (ancestor != null) {
          assert(ancestor is InheritedElement);
          // 添加到依赖项列表
          return inheritFromElement(ancestor, aspect: aspect);
        }
        _hadUnsatisfiedDependencies = true;
        return null;
      }
    

    首先在 _inheritedWidget 映射中查找是否有特定类型 InheritedWidget 的实例。如果有则将该实例添加到自身的依赖列表中,同时将自身添加到对应的依赖项列表中。这样该 InheritedWidget 在更新后就可以通过其 _dependents 属性知道需要通知哪些依赖了它的 widget。

    每当 InheritedElement 实例更新时,会执行实例上的 notifyClients 方法通知依赖了它的子 element 同步更新。notifyClients 实现如下:

    void notifyClients(InheritedWidget oldWidget) {
        if (!widget.updateShouldNotify(oldWidget))
          return;
        assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
        for (Element dependent in _dependents) {
          assert(() {
            // check that it really is our descendant
            Element ancestor = dependent._parent;
            while (ancestor != this && ancestor != null)
              ancestor = ancestor._parent;
            return ancestor == this;
          }());
          // check that it really depends on us
          assert(dependent._dependencies.contains(this));
          dependent.didChangeDependencies();
        }
      }
    

    首先执行相应 InheritedWidget 上的 updateShouldNotify 方法判断是否需要通知,如果该方法返回 true 则遍历 _dependents 列表中的 element 并执行他们的 didChangeDependencies() 方法。这样 InheritedWidget 中的更新就通知到依赖它的子 widget 中了。

      @mustCallSuper
      void didChangeDependencies() {
        assert(_active); // otherwise markNeedsBuild is a no-op
        assert(_debugCheckOwnerBuildTargetExists('didChangeDependencies'));
        // 
        markNeedsBuild();
      }
    

    参考

    Flutter | 状态管理探索篇——Scoped Model
    Flutter实战-第七章 功能型组件
    从 Flutter 源码看 InheritedWidget 内部实现原理

    相关文章

      网友评论

          本文标题:Flutter 第三方库之 Scoped_model

          本文链接:https://www.haomeiwen.com/subject/eozynktx.html