Flutter 第三方库之 Scoped_model

一、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 内部实现原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容