Flutter之万物皆Widget(一种你没见过的方式来深入Widget)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Flutter之万物皆Widget(一种你没见过的方式来深入Widget)

背景


为什么说Flutter万物皆Widget?首先你要知道,Flutter是什么,它是一个现代的响应式框架、一个2D渲染引擎、现成的widget和开发工具,基于Skia,一个性能彪悍的2D图像绘制引擎,2005年被Google收购,被广泛应用于Chrome和Android之上,等等吧,说白一点,Flutter就是一个UI框架,所以说,万物皆Widget,而Widget的中文意思是小部件,它为什么不能像Android或者Ios一样叫做View呢?因为widget既可以是一个结构元素(如按钮或菜单)、也可以是一个文本样式元素(如字体或颜色方案)、布局的一个方面(如填充)等等,我们可以统筹它们为wiget,而不是view,根据基本的命名规范,这就是一种合理的命名抽象。那么接下来我们学什么?

  • Widget是什么
  • Widget类结构
  • 跟着我实现一个widget(直接继承widget抽象类)
  • Element类结构
  • 深入理解Element

Widget是什么


其实上面说了,一切皆Widget,那我们可不可以认为,在flutter的框架中,用到的东西都是Widget呢,当然不是哈,由于它是基于Dart,所以有很多Dart的库,还是可以使用的,比如AES,RSA加密解密,Json序列化等等,但你可以这么说,一切构建图形相关的东西都是Widget,这就是Widget

Widget类结构


为什么说下类结构呢?类结构可以很清晰帮助我们梳理逻辑,从全局的角度看待整个结构

image.png

  • RenderObjectWidget 看名字我们判断,它是持有RenderObject对象的Widget,而通过其他通道了解到,RenderObject实际上是完成界面的布局、测量与绘制,像Padding,Table,Align都是它的子类
  • StatefulWidget 多了一个State状态的Widget,子类都是可以动态改变的如CheckBox,Switch
  • StatelessWidget 就是一个普通的Widget,不可变如Icon,Text。
  • ProxyWidget InheritedWidget就是它的子类,我们暂且认为它是子类能从父类拿数据的关键,以后再研究,大多数的主题都是继承自ProxyWidget

跟我一起实现一个Widget


我不想和别人的教程思路一样,既然万物皆Widget,那我们就从实现一个Widget开始,然后一步步深入,看到什么就去了解什么?来上代码

class TestWidget extends Widget{
  @override
  Element createElement() {
    // TODO: implement createElement
    throw UnimplementedError();
  }
}

创建一个TestWidget然后继承Widget,然后会让你重写函数createElement,返回一个Element,通过这个我们看的出,其实我们创建的Widget,最终肯定是创建了一个Element,那Element到底是什么呢?同样的思路,我们继承Element看一下

class TestElement extends Element{
  TestElement(Widget widget) : super(widget);
  @override
  bool get debugDoingBuild => throw UnimplementedError();
  @override
  void performRebuild() {
  }
}

多了一个构造函数,传递Widget对象,get函数debugDoingBuild,还有performRebuild函数,都是干嘛的呢?

abstract class Element extends DiagnosticableTree implements BuildContext 
abstract class BuildContext {
  /// Whether the [widget] is currently updating the widget or render tree.
  ///
  /// For [StatefulWidget]s and [StatelessWidget]s this flag is true while
  /// their respective build methods are executing.
  /// [RenderObjectWidget]s set this to true while creating or configuring their
  /// associated [RenderObject]s.
  /// Other [Widget] types may set this to true for conceptually similar phases
  /// of their lifecycle.
  ///
  /// When this is true, it is safe for [widget] to establish a dependency to an
  /// [InheritedWidget] by calling [dependOnInheritedElement] or
  /// [dependOnInheritedWidgetOfExactType].
  ///
  /// Accessing this flag in release mode is not valid.
  bool get debugDoingBuild;

经过代码的跟踪我们发现一些注解:

  • Element继承自DiagnosticableTree,并实现BuildContext
  • DiagnosticableTree是个“诊断树”,主要作用是提供调试信息。
  • BuildContext类似原生系统的上下文,它定义了debugDoingBuild,通过注解我们知道,它应该就是一个debug用的一个标志位。
  • performRebuild 经过源码查看后发现,由rebuild()调用如下
void rebuild() {
     if (!_active || !_dirty)
      return;
    performRebuild();
  }
    @override
  void update(ProxyWidget newWidget) {
    rebuild();
  }

首先说明下,这个并不是Element的源码,我摘自StatelessElement,是Element的子类,这说明在update函数后,Element就会直接执行performRebuild函数,那我们完善下自定义的Element逻辑

class TestElement extends Element {
  TestElement(Widget widget) : super(widget);
  @override
  bool get debugDoingBuild => throw UnimplementedError();
  @override
  void performRebuild() {
  }
  @override
  void update(Widget newWidget) {
    super.update(newWidget);
    print("TestWidget update");
    performRebuild();
  }
  @override
  TestWidget get widget => super.widget as TestWidget;
  Widget build() => widget.build(this);
}

在update的时候执行performRebuild(),但是performRebuild执行什么呢?我们结合一下StatelessElement的实现,发现,它调用了传递进来的Widget参数build函数,那么我们就在TestWidget中添加函数,并完善下逻辑后是这样的

class TestWidget extends Widget {
  @override
  Element createElement() {
    /// 将自己传递进去,让Element调用下面的build函数
    return TestElement(this);
  }
   /// 这个context其实就是Element
  Widget build(BuildContext context) {
    print("TestWidget build");
    return Text("TestWidget");
  }
}
class TestElement extends Element {
  Element _child;
  TestElement(Widget widget) : super(widget);
  @override
  bool get debugDoingBuild => throw UnimplementedError();
  @override
  void performRebuild() {
    ///调用build函数
    var _build = build();
    ///更新子视图
   _child =  updateChild(_child, _build, slot);
  }
  @override
  void update(Widget newWidget) {
    super.update(newWidget);
    print("TestWidget update");
    ///更新
    performRebuild();
  }
  ///将widget强转成TestWidget
  @override
  TestWidget get widget => super.widget as TestWidget;
  /// 调用TestWidget的build函数
  Widget build() => widget.build(this);
}

然后将其放入main.dart中如图

image.png

最终效果展示,如图

83d90d6213bd46898bf649b026649fb1_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.jpg

展示出来了,我们简单总结一下,到目前你学到了什么?

  • Widget会创建Element对象(调用createElement并不是Widget,而是Framework)
  • Widget并没有实际的操控UI
  • Element是在update的时候重新调用Widget的build函数来构建子Widget
  • updateChild会根据传入的Widget生成新的Element
  • Widget的函数build,传入的context其实就是它创建的Element对象,那么为什么这么设计呢?一方面它可以隔离掉一些Element的细节,避免Widget频繁调用或者误操作带来的不确定问题,一方面context上下文可以存储树的结构,来从树种查找元素。

其实可以很简单的理解为,Widget就是Element的配置信息,在Dart虚拟机中会频繁的创建和销毁,由于量比较大,所以抽象一层Element来读取配置信息,做一层过滤,最终再真实的绘制出来,这样做的好处就是避免不必要的刷新。接下来我们深入了解下Element

Element类结构


在深入了解Element之前我们也从全局看下它的结构

image.png

可以看到,Element最主要的两个抽象:

  • ComponentElement
  • RenderObjectElement

都是干嘛的呢?经过看源码,发现ComponentElement,其实做了一件事情就是在mount函数中,判断Element是第一次创建,然后调用_firstBuild,最终通过rebuild调用performRebuild,通过上面我们也知道performRebuild最终调用updateChild来绘制UI 而RenderObjectElement就比较复杂一点,它创建了RenderObject,通过RenderObjectWidget的createRenderObject方法,通过以前的学习,我们也知道RenderObject其实是真正绘制UI的对象,所以我们暂且认为RenderObjectElement其实就是可以直接操控RenderObject,一种更直接的方式来控制UI。

深入理解Element


为什么要深入理解Element呢,由于大多数情况下,我们开发者并不会直接操作Element,但对于想要全局了解FlutterUI框架至关重要,特别实在一些状态管理的框架中,如Provider,他们都定制了自己的Element实现,那么这么重要,我们需要从哪方面了解呢?一个很重要的知识点就是生命周期,只有了解了正确的生命周期,你才能在合适的时间做合适的操作

image.png

为了验证该图,我们加入日志打印下,代码如下:

/// 创建LifecycleElement 实现生命周期函数
class LifecycleElement extends TestElement{
  LifecycleElement(Widget widget) : super(widget);
  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
  }
  @override
  void unmount() {
    print("LifecycleElement unmount");
    super.unmount();
  }
  @override
  void activate() {
    print("LifecycleElement activate");
    super.activate();
  }
  @override
  void rebuild() {
    print("LifecycleElement rebuild");
    super.rebuild();
  }
  @override
  void deactivate() {
    print("LifecycleElement deactivate");
    super.deactivate();
  }
  @override
  void didChangeDependencies() {
    print("LifecycleElement didChangeDependencies");
    super.didChangeDependencies();
  }
  @override
  void update(Widget newWidget) {
    print("LifecycleElement update");
    super.update(newWidget);
  }
  @override
  Element updateChild(Element child, Widget newWidget, newSlot) {
    print("LifecycleElement updateChild");
    return super.updateChild(child, newWidget, newSlot);
  }
  @override
  void deactivateChild(Element child) {
    print("LifecycleElement deactivateChild");
    super.deactivateChild(child);
  }
}
class TestWidget extends Widget {
  @override
  Element createElement() {
    /// 将自己传递进去,让Element调用下面的build函数
    /// 更新TestElement为LifecycleElement
    return LifecycleElement(this);
  }
  /// 这个context其实就是Element
  Widget build(BuildContext context) {
    return Text("TestWidget");
  }
}

然后改造下main.dart, 如下

///添加变量
  bool isShow = true;
/// 加入变量控制
  isShow ? TestWidget() : Container(),
/// 将floatingActionButton改为这样的实现
 onPressed: () {
          setState(() {
            isShow = !isShow;
          });
        },

运行一下项目查看日志

image.png

  • 调用 element.mount(parentElement,newSlot)
  • 调用 update(Widget newWidget)
  • 调用 updateChild(Element child, Widget newWidget, newSlot)

然后我们点击下按钮

image.png

  • 调用 deactivate()
  • 调用 unmount()

我们再点击下按钮

image.png

这次只有mount,为什么?由于Widget本身不可变,我判断是因为这个导致的,那如何判断呢?下面介绍一个小技巧,其实flutter的framework层是可以加入调试代码的,我们加入日志看下,如下:

/// widget 基类其实有一个canUpdate函数,我们猜测肯定是这里导致的,加入日志如下
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    if(oldWidget.toString()=="TestWidget") {
      print("canUpdate${oldWidget.runtimeType == newWidget.runtimeType
          && oldWidget.key == newWidget.key}");
    }
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

是个静态函数,肯定是在Element中被调用的,我们找下

@mustCallSuper
  void update(covariant Widget newWidget) {
     if (newWidget.toString() == "TestWidget") {
      print("TestWidget update start");
    }
    assert(_debugLifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && _active
        && Widget.canUpdate(widget, newWidget));
    assert(() {
      _debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
      _debugForgottenChildrenWithGlobalKey.clear();
      return true;
    }());
      if (newWidget.toString() == "TestWidget") {
      print("TestWidget:${newWidget.hashCode}");
    }
    _widget = newWidget;
  }

如上代码是Element的源码,这里调用了canUpdate函数,如果不需要更新的话,就直接中断了执行,我们重新运行下demo,并在加一个print来验证一下newWidget是什么样子的,这里加入newWidget.toString() == "TestWidget",主要是为了过滤垃圾日志,重新运行项目。如图

image.png

点击后按钮

image.png

再点击

image.png

发现并没有调用canUpdate,那我们如何让它重新加载回来呢?我们查查资料,改造下例子

@override
  void mount(Element parent, newSlot) {
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
    assert(_child == null);
    print("LifecycleElement firstBuild");
    performRebuild();
  }

mount函数加入performRebuild()函数,最终会触发updateChild,加assert断言是防止后面再加载进来的时候多次触发updateChild,然后改造下main.dart

@override
  Widget build(BuildContext context) {
    // This method is rerun every time setState is called, for instance as done
    // by the _incrementCounter method above.
    //
    // The Flutter framework has been optimized to make rerunning build methods
    // fast, so that you can just rebuild anything that needs updating rather
    // than having to individually change instances of widgets.
    return Scaffold(
      appBar: AppBar(
        // Here we take the value from the MyHomePage object that was created by
        // the App.build method, and use it to set our appbar title.
        title: Text(widget.title),
      ),
      body: Center(
        // Center is a layout widget. It takes a single child and positions it
        // in the middle of the parent.
        child: isShow ? TestWidget() : Container(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            isShow = !isShow;
          });
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }

去掉Column,这里是由于我们没有处理widget的index逻辑,导致在Column里不正常,后续我们再研究为什么,先来看下生命周期的回调

第一次运行

image.png

点击按钮

image.png

又发现一个问题,为什么我们的断言没生效呢?怎么又出现了firstBuild?哈哈,这里不要纠结,由于TestWidget并非const,导致setState后,又重新被创建了,而对应的Element也同样是创建了新的值,最终导致被重新执行。其实这个TestWidget已经不是上一个了,那我们加入 const修饰再看看

/// 改成const
const TestWidget()
/// 加入当前widget hashcode输出,用来判断两次是否一致
  @override
  void mount(Element parent, newSlot) {
    print("LifecycleElement  widget hashcode${widget.hashCode}");
    print("LifecycleElement hashcode${this.hashCode}");
    print("LifecycleElement mount");
    super.mount(parent, newSlot);
    assert(_child == null);
    print("LifecycleElement firstBuild");
    performRebuild();
  }

最终(启动,点击按钮两次的效果)运行效果如下:

image.png

两次运行Widget保持一致,这就避免了Widget的重建

小结


经过测试我们发现:

  • Widget的创建可以做到复用,通过const修饰
  • Element并没有复用,其实原因应该是在于isShow为false的时候导致其被deactivate 然后unmount,从Element树种被移除掉。
  • 有的人肯定有些疑问,怎么全程没看到activate呢?它不应该属于生命周期的一部分吗?这个就需要用到Key了,在接下来的课程里,讲到Key的时候,我们再详细的学习。

总结


本期我们对Widget,Element有了一个详细的认知,但其实它还有一个State类(StatefulWidget的核心实现)和RenderObject类,这两个下期我再分析。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
29天前
深入理解Flutter鸿蒙next版本 中的Widget继承:使用extends获取数据与父类约束
本文详细介绍了Flutter中如何通过继承其他Widget来创建自定义组件。首先解释了Widget继承的基本概念,包括StatelessWidget和StatefulWidget的区别。接着通过具体示例展示了如何继承StatelessWidget和StatefulWidget,并在子类中访问父类的build方法和状态。最后,结合多个自定义Widget展示了如何在实际应用中灵活使用继承和组合来构建复杂的UI。
74 8
|
27天前
|
容器
flutter&鸿蒙next 使用 InheritedWidget 实现跨 Widget 传递状态
在 Flutter 中,状态管理至关重要。本文详细介绍了如何使用 InheritedWidget 实现跨 Widget 的状态传递。InheritedWidget 允许数据在 Widget 树中向下传递,适用于多层嵌套的场景。通过一个简单的计数器示例,展示了如何创建和使用 InheritedWidget,包括其基础概念、工作原理及代码实现。虽然 InheritedWidget 较底层,但它是许多高级状态管理解决方案的基础。
96 2
|
2月前
|
容器
flutter:第一个flutter&Widget的使用 (二)
本文介绍了Flutter框架下的基本组件及其用法,包括简单的 Stateless Widget 如文本和按钮,以及更复杂的 StatefulWidget 示例。详细解释了如何使用 `context` 获取祖先小部件的信息,并展示了 `MaterialApp` 的属性及用途。此外,还探讨了 `StatefulWidget` 与 `StatelessWidget` 的区别,以及 `AppBar` 的常见属性配置方法。适合Flutter初学者参考学习。
|
27天前
|
Dart JavaScript 前端开发
Flutter 的 Widget 概述与常用 Widgets 与鸿蒙 Next 的对比
Flutter 是 Google 开发的开源 UI 框架,用于快速构建高性能的移动、Web 和桌面应用。Flutter 通过 Widget 构建 UI,每个 UI 元素都是 Widget,包括文本、按钮、图片等。Widget 不仅描述外观,还描述行为,是不可变的。常见的 Widget 包括结构型(Container、Column、Row)、呈现型(Text、Image)、交互型(ElevatedButton)和状态管理型(StatefulWidget)。Flutter 与鸿蒙 Next 在组件化架构、开发语言、布局系统、性能和跨平台支持方面各有优势
73 0
|
5月前
Flutter-底部弹出框(Widget层级)
文章描述了如何在Flutter中使用DraggableScrollableSheet创建一个底部弹出框,同时保持其可手势滑动关闭。作者遇到问题并提出对原控件进行扩展,以支持头部和列表布局的滑动关闭功能。
189 0
|
6月前
Flutter StreamBuilder 实现局部刷新 Widget
Flutter StreamBuilder 实现局部刷新 Widget
47 0
|
7月前
|
Android开发
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
Flutter完整开发实战详解(六、 深入Widget原理),2024百度Android岗面试真题收录解析
|
7月前
|
开发框架 前端开发 搜索推荐
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
【4月更文挑战第30天】探索Flutter的自定义Widget与渲染流程。自定义Widget是实现复杂UI设计的关键,优点在于个性化设计、功能扩展和代码复用,但也面临性能优化和复杂性管理的挑战。创建步骤包括设计结构、定义Widget类、实现构建逻辑和处理交互。Flutter渲染流程涉及渲染对象树、布局、绘制和合成阶段。实践案例展示如何创建带渐变背景和阴影的自定义按钮。了解这些知识能提升应用体验并应对开发挑战。查阅官方文档以深入学习。
84 0
【Flutter前端技术开发专栏】Flutter中的自定义Widget与渲染流程
|
7月前
|
JavaScript 前端开发 开发者
【Flutter前端技术开发专栏】Flutter中的Widget与状态管理
【4月更文挑战第30天】本文探讨了Flutter的Widget和状态管理。Widget是Flutter构建UI的基础,分为有状态和无状态两种。状态管理确保UI随应用状态变化更新,影响应用性能和可维护性。文章介绍了`setState`、`Provider`、`Riverpod`、`Bloc`和`Redux`等状态管理方法,并通过计数器应用展示了其实现。选择合适的状态管理策略对高效开发至关重要。
96 0
【Flutter前端技术开发专栏】Flutter中的Widget与状态管理
|
7月前
|
前端开发 开发者 UED
Flutter的自定义Painter:深入探索自定义绘制Widget的技术实现
【4月更文挑战第26天】Flutter的自定义Painter允许开发者根据需求绘制独特UI,通过继承`CustomPaint`类和重写`paint`方法实现。在`paint`中使用`Canvas`绘制图形、路径等。创建自定义Painter类后,将其作为`CustomPaint` Widget的`painter`属性使用。此技术可实现自定义形状、渐变、动画等复杂效果,提升应用视觉体验。随着Flutter的进化,自定义Painter将提供更丰富的功能。