likes
comments
collection
share

const关键字声明的Widget不刷新?

作者站长头像
站长
· 阅读数 7

前言

最近进行CodeReview的时候发现了一个比较有意思的小bug

先说下前提场景:在使用Provider作为状态管理的时候,某一个Widget中使用context.read去获取数据进行展示

案例Sample

来,上点超简单的代码示例

TestPage

class TestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<TestChanger>(
      create: (ctx) {
        return TestChanger();
      },
      child: Consumer<TestChanger>(
        builder: (ctx, vm, child) {
          return Scaffold(
            body: Center(
              child: ElevatedButton(
                onPressed: (){
                  vm.doIncrease();
                },
                child: const Text("increase"),
              ),
            ),
            bottomNavigationBar: const TestBottomWidget(),
          );
        },
      ),
    );
  }
}

这是页面的Widget,可以看到页面就两个元素,中间有一个响应点击事件的文本,底部有一个TestBottomWidget ,接着来看看这个底部Widget

TestBottomWidget

class TestBottomWidget extends StatelessWidget {
  const TestBottomWidget({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    TestChanger vm = context.read<TestChanger>();
    return Container(
      height: 50,
      color: Colors.orange,
      child: Center(
        child: Text("count = ${vm.count.toString()}"),
      ),
    );
  }
}

更简单,就是通过BuildContextread到对应的ChangeNotifier,进行展示,最后就是状态类:

TestChanger

class TestChanger extends ChangeNotifier {
  int count = 0;

  doIncrease() {
    count++;
    notifyListeners();
  }
}

存储一个count值,点击时进行++并对外通知;


现象

先看看效果

const关键字声明的Widget不刷新?

可以看到increase按钮都点冒烟了下面的count数字也不变,这是为什么呢?

明明ChangeNotifier执行了notifyListeners,接着Page中的Consumerbuilder也重新执行了,那么bottomNavigationBar指向的TestBottomWidget也就应该是个新的Widget,那么其build方法也就应该执行,展示的count值就应该是最新的…

分析

这里我们要注意我们对于Scaffold#bottomNavigationBar的值声明:

bottomNavigationBar: const TestBottomWidget(),

在这里使用了const关键字,问题的根源就在这里;

const

我们来了解一下const对象在Dart虚拟机中会怎么样分配:

在Dart虚拟机中,不同于JVM的内存分区设计(堆、栈、静态方法区、本地方法区等),不过也存在堆、栈这两大核心重要的区域。DVM的堆内存,一样也是用于存储动态分配的对象的区域。这包括通过 newconst 关键字创建的所有对象;

const对象是存在于堆内存中的一块单独区域当中,可以理解为一个“常量池”;在Dart中,对于这些变量,会进行一系列的判断来决定是否可以复用池中的对象而不是新建一个。这其中会包括参数、方法域等判断,如果都一致的情况下,是不会去堆中再创建一个对象的。在我们这个例子中,TestBottomWidget就会被DVM认为可以去“常量池”中进行复用。

到这里我们就明白了,原来每次Scaffold更新时,看似我们传递了一个新的TestBottomWidget对象,但实际上这个TestBottomWidget 一直都指向同一个地址。那么紧接着就是第二个问题,虽然对象是同一个,那么既然它的上级Widget执行了build方法,也应该驱动这个Widget刷新,去执行build方法,那应该也能通过contextread到最新的值才对,为什么不变化呢?

通过断点可以发现,TestBottomWidgetbuild方法根本不会执行!

这就需要我们到framework代码中去找答案了;

Element update

Flutter中Widgetbuild方法是由其Element去驱动调用的,Element的更新核心逻辑在Element#updateChild方法当中,这个方法决定了此次更新当前的Element需不需要重建,是否需要重新调用其自身的update方法

来看看具体代码:

const关键字声明的Widget不刷新?

判断共分为4大分支,首先判断了当前Element(即child对象)是否存在,如果不存在(如第一次运行),则直接进入分支4,去执行inflateWidget,通过Widget去创建一个Element;否则则是Element已经存在,也就是之前创建过这个Element,本次进入该方法是组件树进行了刷新;

那么前置判断了ElementWidget是否属于同一类别,得到一个boolhasSameSuperclass,这个同类别是指,StatelessWidget要对应StatelessElementStatefulWidget要对应StatefulElement。接着我们看这个分支判断逻辑

  • 分支1:如果是同一类别,且Widget对象为同一个(对象指针地址),则不做任何更新动作(这里的slot暂不分析)
  • 分支2:如果是同一类别,且当前Element对应的Widget和新Widget的运行时类型和key一致(即代码中的Widget.canUpdate方法),则认为当前Element仍可使用,只是需要拿新的Widget所携带的信息去更新该Element,此流程就会触发到StatelessWidgetbuild方法
  • 分支3:如果不属于上述两种情况,则认为Element不可复用,直接拿newWidget去创建一个新的Element

分析到这里,根据我们前面分析的const关键字,我们就知道了前因后果,我们每次点击页面中间按钮去更新count值并刷新页面的时候,当页面组件树更新到了TestBottomWidget的上级Element,该去判断TestBottomWidget要不要更新时,由于const关键字声明了Widget,所以从始至终就只有一个该对象,遂进入了上述的代码分支1,最终TestBottomWidgetbuild方法不会执行,也就不会更新展示最新count值。

写在最后

首先,这个问题并不局限于使用Provider时才存在,任何这种组件自身内使用context或其他不依赖构造传参方式去动态获取数据并使用的场景下,都存在这种情况。归根结底就是组件自身的更新无法被父级驱动(因为父级认为你不需要更新)。

那么为什么要给Widget声明const关键字呢?

const关键字声明的Widget不刷新?

因为我们这个Widget没有接受任何变量,IDE认为我们这个Widget可以使用const构造,也就可以在声明时增加const关键字。。。

显然IDE没有想到我们这个Widget虽然没有变量参数,但其内的展示是使用contextread了一些变量,并不是一个静态Widget😮‍💨

在这个例子中,怎么解决呢?

  • 去除const声明
  • TestBottomWidget内,不能声明read去获取值了,因为read并不会把自己注册在Provider刷新组件当中,仅仅是标记只读;可以使用watch去代替,或者在组件的build方法中返回时包裹一个ConsumerSelector去注册刷新

对于上述的第二种解决方案,其实就是换了种方式去驱动自身刷新。

最后的总结:

  • const关键字声明的对象在DVM中会进行一系列的判断,通过方法域、参数值、对象上下文等因素,会进行对象的复用,不会新创建对象
  • Element的更新逻辑中如果此次更新时Widget和当前Element持有的Widget是同一对象,则不会进行更新

补充一个github地址,可以clone下来试试看,也可以直接粘贴文章开头的代码块