颤动:如何正确使用继承的小部件?

时间:2018-03-26 12:47:46

标签: dart flutter

使用InheritedWidget的正确方法是什么?到目前为止,我明白它让你有机会沿着Widget树传播数据。在极端情况下如果你把它作为RootWidget,它将可以从所有Routes树中的所有Widgets访问,这很好,因为不知何故我必须使我的ViewModel / Model可以访问我的Widgets而不必求助于全局或Singletons。

但是InheritedWidget是不可变的,那么如何更新呢?更重要的是,我的状态窗口小部件如何触发重建其子树?

不幸的是,文档在这里非常不清楚,经过大量讨论后,似乎没有人真正知道使用它的正确方法。

我添加了Brian Egan的一句话:

  

是的,我认为这是一种沿树传播数据的方法。我发现了什么   困惑,来自API文档:

     

"当以这种方式引用时,继承的小部件将导致   当继承的窗口小部件本身改变状态时,消费者重建。"

     

当我第一次阅读本文时,我想:

     

我可以在InheritedWidget中填充一些数据并稍后进行修改。   当突变发生时,它将重建所有的小部件   引用我的InheritedWidget我发现了什么:

     

为了改变InheritedWidget的状态,你需要换行   它在StatefulWidget中你实际上改变了状态   StatefulWidget并将此数据传递给InheritedWidget,后者   将数据交给所有孩子。但是,在那种情况下,它   似乎重建了StatefulWidget下面的整个树,而不是   只是引用InheritedWidget的Widgets。那是对的吗?   或者它会以某种方式知道如何跳过引用它的小部件   如果updateShouldNotify返回false,则为InheritedWidget吗?

4 个答案:

答案 0 :(得分:56)

问题来自你的引用,这是不正确的。

正如你所说,InheritedWidgets和其他小部件一样,是不可变的。因此,他们不会更新。它们是重新创造的。

事情是: InheritedWidget只是一个简单的小部件,只能保存数据。它没有任何更新逻辑或任何。 但是,与任何其他小部件一样,它与Element相关联。 你猜怎么着 ?这个东西是可变的,只要有可能就会重复使用它!

更正后的报价为:

  

当以这种方式引用时,InheritedWidget将导致使用者在与 InheritedElement 关联的InheritedWidget更改时重建。

There's a great talk about how widgets/elements/renderbox are pluged together。 但简而言之,它们就像这样(左边是典型的小部件,中间是'元素',右边是'渲染框'):

enter image description here

事情是:当你实例化一个新的小部件时;扑动将它与旧的比较。重用它的“元素”,它指向一个RenderBox。 mutate renderbox属性。

Okey,但这是如何回答我的问题的呢?

嗯,这很简单。实例化一个InheritedWidget,然后调用context.inheritedWidgetOfExactType(或基本相同的MyClass.of);隐含的是它会听取与您Element相关联的InheritedWidget。每当Element获得一个新的小部件时,它将强制刷新调用前一个方法的任何小部件。

简而言之,当您用全新的InheritedWidget替换现有的InheritedWidget时;颤动会看到它改变了。并且会通知可能修改的绑定小部件。

如果你理解了一切,你应该已经猜到了解决方案:

StatefulWidget包裹在InheritedWidget内,只要有变化,就会创建一个全新的InheritedWidget! 在这种情况下,建议您的StatefulWidget数据实际上只是InheritedWidget的实例,然后将class MyInherited extends StatefulWidget { Widget child; MyInherited({this.child}); @override MyInheritedState createState() => new MyInheritedState(); static MyInheritedState of(BuildContext context) { return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data; } } class MyInheritedState extends State<MyInherited> { String _myField; // only expose a getter to prevent bad usage String get myField => _myField; void onMyFieldChange(String newValue) { setState(() { _myField = newValue; }); } @override Widget build(BuildContext context) { return new _MyInherited( data: this, child: widget.child, ); } } /// Only has MyInheritedState as field. class _MyInherited extends InheritedWidget { final MyInheritedState data; _MyInherited({Key key, this.data, Widget child}) : super(key: key, child: child); @override bool updateShouldNotify(_MyInherited old) { return true; } } 设为私有。避免不必要的复制粘贴和可能的错误。

实际代码的最终结果是:

class Program
{
    static DataTable Product = new DataTable("Product");
    static DataTable OrderHistory = new DataTable("OrderHeader");
    static DataTable OrderLines = new DataTable("OrderLines");
    static DataTable CustomerHistory = new DataTable("CustomerHistory");

   public static void LoadTables()
    {
        Product.Columns.AddRange(new DataColumn[]
        {
            new DataColumn("ProductID",typeof(int)),
            new DataColumn("ProductCode",typeof(int)),
            new DataColumn("Rank",typeof(int))
        });
        Product.Rows.Add(200001, 10001, 1);
        Product.Rows.Add(200002, 10002, 2);
        Product.Rows.Add(200003, 10003, 3);
        Product.Rows.Add(200004, 10004, 4);
        Product.Rows.Add(200005, 10005, 5);
        Product.Rows.Add(200006, 10006, 6);
        Product.Rows.Add(200007, 10007, 7);
        Product.Rows.Add(200008, 10008, 8);

        OrderHistory.Columns.AddRange(new DataColumn[]
        {
            new DataColumn("CustomerID", typeof(string)),
            new DataColumn("OrderID", typeof(int))
        });
        OrderHistory.Rows.Add("ABC001", 123);
        OrderHistory.Rows.Add("ABC002", 124);
        OrderHistory.Rows.Add("ABC003", 125);
        OrderHistory.Rows.Add("ABC004", 126);
        //OrderHistory.Rows.Add("BA009", 127);

        OrderLines.Columns.AddRange(new DataColumn[]
        {
            new DataColumn("OrderID",typeof(int)),
            new DataColumn("ProductID", typeof(int))
        });
        OrderLines.Rows.Add(123, 200001);
        OrderLines.Rows.Add(123, 200002);
        OrderLines.Rows.Add(123, 200003);
        OrderLines.Rows.Add(123, 200004);
        OrderLines.Rows.Add(123, 200005);
        OrderLines.Rows.Add(124, 200006);
        OrderLines.Rows.Add(124, 200006);
        OrderLines.Rows.Add(124, 200007);
        OrderLines.Rows.Add(124, 200008);
        OrderLines.Rows.Add(124, 200009);
        OrderLines.Rows.Add(127, 200001);
        OrderLines.Rows.Add(127, 200002);
        OrderLines.Rows.Add(127, 200009);
        OrderLines.Rows.Add(127, 200008);
        OrderLines.Rows.Add(127, 200007);
        OrderLines.Rows.Add(126, 200006);
        OrderLines.Rows.Add(126, 200005);
        OrderLines.Rows.Add(126, 200008);
    }

    static void Main(string[] args)
    {
        LoadTables();

        var rows = (from orderLines in OrderLines.AsEnumerable()
                    join orderHistory in OrderHistory.AsEnumerable()
                    on orderLines["OrderID"] equals orderHistory["OrderID"]
                    select new
                    {
                        CustomerID = orderHistory["CustomerID"],
                        ProductID = orderLines["ProductID"]
                    }).Distinct();

        CustomerHistory.Columns.AddRange(new DataColumn[]
        {
            new DataColumn("CustomerID"),
            new DataColumn("ProductID")
        });

        foreach (var row in rows)
        {
            CustomerHistory.Rows.Add(row.CustomerID, row.ProductID);
        }

        //foreach (DataRow row in CustomerHistory.Rows)
        //{
        //    Console.WriteLine(row[0].ToString() + "\t" + row[1].ToString());
        //}

        var topSellers = "Select Top250 CustomerHistory.CustomerID, Product.ProductCode";

    }
}

但是不会创建一个新的InheritedWidget重建整个树吗?

不,它不会是必然的。因为您的新InheritedWidget可能具有与以前完全相同的子级。确切地说,我的意思是同一个例子。 具有相同实例的小部件不会重建。

在大多数情况下(在应用程序的根目录中有一个inheritedWidget),继承的小部件是常量。所以没有不必要的重建。

答案 1 :(得分:8)

TL; DR

在创建小部件

时,不要在 updateShouldNotify 方法内使用大量计算,而应使用 const 代替 new

首先,我们应该了解什么是Widget,Element和Render对象。

  1. 渲染对象是屏幕上实际渲染的对象。它们是可变的,包含绘画和布局逻辑。渲染树与网络中的文档对象模型(DOM)非常相似,您可以在此树中将渲染对象视为DOM节点
  2. 窗口小部件-描述应呈现的内容。它们不可变且便宜。因此,如果Widget回答问题“ What?”(声明性方法),则Render对象回答问题“ How?”(命令性方法)。网络上的类比是“虚拟DOM”。
  3. Element / BuildContext -是小部件 Render 对象之间的代理。它包含有关小部件在树中的位置*以及更改相应小部件时如何更新Render对象的信息。

现在,我们准备深入了解 InheritedWidget 和BuildContext的方法 inheritFromWidgetOfExactType

作为示例,我建议我们参考Flutter文档中有关InheritedWidget的示例:

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  })  : assert(color != null),
        assert(child != null),
        super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(FrogColor);
  }

  @override
  bool updateShouldNotify(FrogColor old) {
    return color != old.color;
  }
}

InheritedWidget-仅是在我们的示例中实现一种重要方法- updateShouldNotify 的小部件。 updateShouldNotify -一个接受一个参数 oldWidget 并返回布尔值:true或false的函数。

与任何小部件一样, InheritedWidget 具有对应的Element对象。它是 InheritedElement 。每次我们构建新的小部件时,InheritedElement都会在小部件上调用 updateShouldNotify (在祖先上调用 setState )。当 updateShouldNotify 返回 true 时,InheritedElement遍历 dependencies (?)并对其调用方法 didChangeDependencies

InheritedElement在哪里获得依赖?在这里,我们应该看看 inheritFromWidgetOfExactType 方法。

inheritFromWidgetOfExactType -在BuildContext和 每个元素实现BuildContext接口(Element == BuildContext)。因此,每个元素都有此方法。

让我们看一下InheritFromWidgetOfExactType的代码:

final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
  assert(ancestor is InheritedElement);
  return inheritFromElement(ancestor, aspect: aspect);
}

在这里,我们尝试在 _inheritedWidgets 中按类型映射的祖先。 如果找到祖先,则我们调用 inheritFromElement

inheritFromElement 的代码:

  InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
    assert(ancestor != null);
    _dependencies ??= HashSet<InheritedElement>();
    _dependencies.add(ancestor);
    ancestor.updateDependencies(this, aspect);
    return ancestor.widget;
  }
  1. 我们将祖先添加为当前元素(_dependencies.add(ancestor))的依赖项
  2. 我们将当前元素添加到祖先的依赖项中(ancestor.updateDependencies(this,Aspect))
  3. 我们根据 inheritFromWidgetOfExactType (返回ancestor.widget)
  4. 的结果返回祖先的小部件。

所以现在我们知道InheritedElement从哪里获取依赖项。

现在让我们来看一下 didChangeDependencies 方法。 每个元素都有此方法:

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

我们可以看到此方法只是将元素标记为 dirty ,并且应在下一帧重建该元素。 Rebuild 表示在核心响应小部件元素上调用方法 build

但是“重新构建InheritedWidget时整个子树是否重新构建?”呢? 在这里,我们应该记住,小部件是不可变的,如果您创建新的小部件,Flutter将重建子树。我们该如何解决?

  1. 手动缓存小部件
  2. 使用 const ,因为const create the only one instance的值/类

答案 2 :(得分:1)

来自docs

  

[BuildContext.inheritFromWidgetOfExactType]获取最近的窗口小部件   给定类型的,必须是混凝土的类型   InheritedWidget子类,并向其注册该构建上下文   小部件,以便当该小部件更改时(或该小部件的新小部件   类型被引入,或者小部件消失了),这个构建上下文是   重新构建,以便它可以从该小部件获取新值。

     

这通常是从of()静态方法中隐式调用的,例如   主题

正如OP所指出的,InheritedWidget实例不会更改...但是可以用小部件树中相同位置的新实例替换它。发生这种情况时,可能需要重建已注册的窗口小部件。 InheritedWidget.updateShouldNotify方法进行此确定。 (请参阅:docs

那么如何替换实例? InheritedWidget可以包含一个StatefulWidget实例,该实例可以用新实例替换旧实例。

答案 3 :(得分:0)

InheritedWidget管理应用程序的集中式数据并将其传递给子级,就像我们可以在此处存储购物车计数一样,如here所述: