Flutter 内一切皆 Widget ,Widget 是不可变的(immutable),每个 Widget 状态都代表了一帧。

    理解这段话是非常重要的,这句话也是很多一开始接触 Flutter 的开发者比较迷惑的地方,因为 Flutter 中所有界面的展示效果,在代码层面都是通过 Widget 作为入口开始。

    Widget 是不可变的,说明页面发生变化时 Widget 一定是被重新构建, Widget 的固定状态代表了一帧静止的画面,当画面发生改变时,对应的 Widget 一定会变化。

    举个我经常说的例子,如下代码所示定义了一个 TestWidget,TestWidget 接受传入的 title 和 count 参数显示到 Text 上,同时如果 count 大于 99,则只显示 99。

    1. /// Warnning
    2. /// This class is marked as '@immutable'
    3. /// but one or more of its instance fields are not final
    4. class TestWidget extends StatelessWidget {
    5. final String title;
    6. int count;
    7. TestWidget({this.title, this.count});
    8. @override
    9. Widget build(BuildContext context) {
    10. this.count = (count > 99) ? 99 : count;
    11. return Container(
    12. child: new Text("$title $count"),
    13. );
    14. }
    15. }

    这段代码看起来没有什么问题,也可以正常运行,但是在编译器上会有 “This class is marked as ‘@immutable’,but one or more of its instance fields are not final” 的提示警告,这是因为 TestWidget 内的 count 成员变量没有加上 final 声明,从而在代码层面容易产生歧义。

    因为前面说过 Widget 是 immutable ,所以它的每次变化都会导致自身被重新构建,也就是 TestWidget 内的 count 成员变量其实是不会被保存且二次使用。

    如上所示代码中 count 成员没有 final 声明,所以理论是可以对 count 进行二次修改赋值,造成 count 成员好像被保存在 TestWidget 中被二次使用的错觉,容易产生歧义,比如某种情况下的 widget.count,所以需要加这个 final 就可以看出来 Widget 的不可变逻辑。

    如果把 StatelessWidget 换成 StatefulWidget ,然后把 build 方法放到 State 里,State 里的 count 就可以就可以实现跨帧保存。

    1. class TestWidgetWithState extends StatefulWidget {
    2. final String title;
    3. TestWidgetWithState({this.title});
    4. @override
    5. _TestWidgetState createState() => _TestWidgetState();
    6. }
    7. class _TestWidgetState extends State<TestWidgetWithState> {
    8. int count;
    9. @override
    10. Widget build(BuildContext context) {
    11. this.count = (count > 99) ? 99 : count;
    12. return InkWell(
    13. onTap: () {
    14. setState(() {
    15. count++;
    16. });
    17. },
    18. child: Container(
    19. child: new Text("${widget.title} $count"),
    20. ),
    21. );
    22. }
    23. }

    所以这里最重要的是,首先要理解 Widget 的不可变性质,然后知道了通过 State 就可以实现数据的跨 Widget 保存和恢复,那为什么 State 就可以呢?

    这就涉及到 Flutter 中另外一个很重要的知识点,Widget 的背后又是什么?事实上在 Flutter 中 Widget 并不是真正控件,在 Flutter 的世界里,我们最常使用的 Widget 其实更像是配置文件,而在其后面的 Element 、RenderObject 、Layer 等才是实际“干活”的对象。

    Element 、RenderObject 、Layer 才是需要学习理解的对象。

    简单举个例子,如下代码所示,其中 testUseAll 这个 Text 在同一个页面下在三处地方被使用,并且代码可以正常运行渲染,如果是一个真正的 View ,是不能在一个页面下这样被多个地方加载使用的。

    image.png

    在 Flutter 设定里,Widget 是配置文件告诉 Flutter 你想要怎么渲染, Widget 在 Flutter 里会经过 Element 、RenderObject、乃至 Layer 最终去进行渲染,所以作为配置文件的 Widget 可以是 @immutable,可以每次状态更新都被重构。

    所以回到最初说过的问题:Flutter 的嵌套很恶心?是的 Flutter 设定上确实导致它会有嵌套的客观事实,但是当你把 Widget 理解成配置文件,你就可以更好地组织代码,比如 Flutter 里的 Container 就是一个抽象的配置模版。

    参考 Container 你就学会了 Flutter 组织代码逻辑的第一步。

    同时因为 Widget 并不是真正干活的,所以嵌套事实上并不是嵌套 View ,一般情况下 Widget 的嵌套是不会带来什么性能问题,因为它不是正式干活的,嵌套不会带来严重的性能损失。

    举个例子,当你写了一堆的 Widget 被加载时,第一次会对应产生出 Element ,之后 Element 持有了 Widget 和 RenderObject。

    简单的来说,一般情况下画面的改变,就是之后 Widget 的变化被更新到 RenderObject ,而在 Flutter 中能够跨帧保存的 State ,其实也是被 Element 所持有,从而可以用来跨 Widget 保存数据。

    所以 Widget 的嵌套一般不会带来性能问题,每个 Widget 状态都代表了一帧,可以理解为这个“配置信息”代表了当前的一个画面,在 Widget 的背后,嵌套的 Padding 、Align 这些控件,最后只是 canvas 时的一个“偏移计算”而已。

    所以理解 Widget 控件很重要,Widget 不是真正的 View ,它只是配置信息,只有理解了这点,你才会发现 Flutter 更广阔的大陆,比如:

    • Flutter 的控件是从 Elemnt 才开始是真正的工作对象;
    • 要看一个 Widget 的界面效果是怎么实现,应该去看它对应的 RenderObejcet 是怎么绘制的;
    • 要知道不同堆栈或者模块的页面为什么不会互相干扰,就去看它的 Layer 是什么逻辑;
    • 是不是所有的 Widget 都有 RenderObejcet ? Widget 、 Elemnt 、RenderObejcet 、Layer 的对应关系是什么?

    这些内容才是学 Flutter 需要如理解和融汇贯通的,当你了解了关于 Widget 背后的这一套复杂的逻辑支撑后,你就会发现 Flutter 是那么的简单,在实现复杂控件上是那么地简单,Canvas 组合起来的能力是真的香。