其他分享
首页 > 其他分享> > Flutter学习 Widget简介

Flutter学习 Widget简介

作者:互联网

目录

1. Widget 概述

1.1 Widget概念

在 Flutter 中,几乎所有的对象都是一个 Widget ,与原生的“控件”的,Flutter 中的 Widget 是一个更广泛的概念,正所谓一切皆可Widget, 它不仅可以表示 UI 元素,也可以表示一些功能性的组件,例如 Theme、GuestureDector等。

Flutter 的 Widget 其实就是 “组件”、“部件”、“控件”的概念, 因为其实际灵感是来源于 React, 所以其目标就是通过 Widget 嵌套 Widget 的方式来构建UI和进行逻辑处理。

和 Android 的View相比,Widget 粗略的可以相当于View, Widget 和 View最大的不同是:Widget具有不同的生命周期,每当 Widget 或其状态状态发生变化时, Flutter 的框架都会创建一个新的 Widget实例树, 相比之下,Android 中的 View 会被绘制一次,并且在 invalidate 调用之前不会重绘。

1.2 Widget 分类

因为万物皆可 Widget, 所以 Widget 承载了基本所有的业务,自然而然也有各种各样的Widget,分类也有很多,主要包括下面这些类别:

Basics 比较特殊, 它并不是一个专门的类别组件,而是从其他官方Widget类中,选取一些常用的、易用的组件组成的类别,例如 Row 属于 Layout 组件的东西,但它也被选进了 Basics。

所以官方的意图是,在你开始构建第一个 Flutter 应用前,你可以通过学习 Basics 基础组件,来了解一些最常用的开发组件和知识。

Widget 更多的是以组合的形式存在,这其实体现良好的设计思想,因为在很多场景中,组合的设计结构是要比继承的结构好的。
例如 Container 是属于 Layout组件中的一个 Widget, 而 Container 又有 LimitedBox、ConstrainedBox、Aligin、Padding、DecoratedBox、 Transform 等部件来组成。如果想要实现 Container 的自定义效果,可以组合上面这些 Widget 以及其他简单的 Widget, 而不是把它写成某个Layout组件的子类,这样做的好处是:

  1. 这样不会限制它的行为
    类比 Android,一个实现了复杂的效果的 Button视图 如果是继承的 FrameLayout,你会觉得它被限制了很多行为,它看起来是一个 Button,但它的逻辑却是一个 Layout
  2. 少写胶水代码
    例如上一点,本来想要给 Button 设置一个 Text,但是 FrameLayout 没有 setText方法,只能写这种胶水代码,来调用 Button 的 setText 方法

2. Widget 接口

在 Flutter 中, Widget 的功能是 “描述一个 UI 元素的配置信息”,也就是说 Widget 并不是表示最终绘制在设备屏幕上的显示元素,比如对 Text 来讲,文本的内容、文本样式等都是他的配置信息,来通过下面 Widget 代码,来看下一些 Widget使用到的接口:

@immutable // 不可变的
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key? key;

  @protected
  @factory
  Element createElement();

  @override
  String toStringShort() {
    final String type = objectRuntimeType(this, 'Widget');
    return key == null ? type : '$type-$key';
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
  }

  @override
  @nonVirtual
  bool operator ==(Object other) => super == other;

  @override
  @nonVirtual
  int get hashCode => super.hashCode;

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
  ...
}

Widget 本身是一个抽象类,其中最核心的就是定义了 createElement() 接口。在 Flutter 开发中,我们不会直接继承 Widget 类来实现组件,而是继承 StatelessWidget 或者 StatefulWidget 来间接继承 Widget 类。接下来来重点介绍这两个类。

3. StatelessWidget 和 StatefulWidget

3.1 Flutter 中的四棵树

来看看 Flutter 框架的处理流程:

  1. 根据 Widget 树生成一个 Element 树, Element 树中的节点都继承自 Element
  2. 根据 Element 树生成 Render 树(即渲染树), 渲染树中的节点都继承自 RenderObject
  3. 根据 渲染树 生成 Layer 树 ,然后上屏显示, Layer树中的节点都继承自 Layer

也就是说,真正的布局和渲染逻辑在 Render树中, Element 是 Widget 和 RenderObject 的中间态,用下面例子来说明,假设有一个 Widget 树:

Container( // 一个容器 widget
  color: const Color.fromRGBO(0, 0, 100, 1), // 设置容器背景色
  child: Row( // 可以将子widget沿水平方向排列
    children: [
      Image.network('https://www.example.com/1.png'), // 显示图片的 widget
      const Text('A'),
    ],
  ),
);

如果 Container 设置了背景色, Container 内部会创建一个新的 ColoredBox 来填充背景,相关逻辑如下:

if (color != null)
  current = ColoredBox(color: color!, child: current);

Image 内部会通过 RawImage 来渲染图片、 Text 内部会通过 RichText 来渲染文本,所以最终的 Widget树、 Element树、渲染树如下图所示:
在这里插入图片描述
这里需要注意的是:

3.2 StatelessWidget

StatelessWidget 继承自 Widget 类,重写了 createElement() :

@override
StatelessElement createElement() => StatelessElement(this);

StatelessElement 间接继承自 Element 类, 与 StatelessWidget 是对应的。

StatelessWidget 的作用域是不需要维护状态的场景,它通常在 build 方法中通过嵌套其它 Widget 来构建UI,在构建过程中会递归的构建其嵌套的 Widget。 也就说它的一个主要场景是作为根布局容器。

来看下面一段官方代码:

class Echo extends StatelessWidget  {
  const Echo({
    Key? key,  
    required this.text,
    this.backgroundColor = Colors.grey, //默认为灰色
  }):super(key:key);
    
  final String text;
  final Color backgroundColor;

  @override
  widget build(BuildContext context) {
    return Center(
      child: Container(
        color: backgroundColor,
        child: Text(text),
      ),
    );
  }
}

上述代码实现了一个显示字符串的 Widget。

这里有几个注意的点:

然后我们可以在别的 Widget 里面通过如下方式使用它:

Widget build(BuildContext context) {
  return Echo(text: "hello world");
}

如下所示:
在这里插入图片描述

3.2.1 Context

build() 中有一个 BuildContext 的传参,它是 BuildContext 类的一个实例,表示当前 Widget 在 Widget 树中的上下文,每一个 Widget 都有一个 Context对象。 实际上 context 是当前Widget 在 Widget 树中位置执行“相关操作”的一个句柄, 比如它提供了从当前 Widget 开始向上遍历 Widget 树以及按照 Widget 类型 查找父级 Widget 的方法。 下面是在 子树中获取父级 Widget 的一个示例:

class ContextRoute extends StatelessWidget  {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Context测试"),
      ),
      body: Container(
        child: Builder(builder: (context) {
          // 在 widget 树中向上查找最近的父级`Scaffold`  widget 
          Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
          // 直接返回 AppBar的title
          return (scaffold.appBar as AppBar).title;
        }),
      ),
    );
  }
}

3.3 StatefulWidget

StatefulWidget 也是继承了 Widget 类, 并重写了 createElement() 方法, 它返回的是一个 StatefulEment 对象。 另外 StatefulWidget 添加了一个新的接口 createState():

abstract class StatefulWidget extends Widget {

  const StatefulWidget({ Key? key }) : super(key: key);

  @override
  StatefulElement createElement() => StatefulElement(this);

  @protected
  @factory
  State createState(); 

在 StatefulWidget 中, State 对象和 StatefulElement 具有一一对应的关系。所以在 Flutter 的 SDK 中,经常能看到注释:“从树中移除 State 对象” 或 “插入 State 对象”, 这里的树指的就是 Element 树。

3.4 State

State 表示的是预期对应的 StatefulWidget 要维护的状态, State中的保存的状态信息可以:

  1. 在 Widget 构建时可以被同步读取
  2. 在 Widget 生命周期中可以改变,改变时, 可以手动调用其 setState() 方法通知 Flutter 框架状态发生改变, Flutter 框架在接收到消息后,会重新调用 StatefulWidget.build 重新构建 Widget 树,已达到更新 UI 的目的

State 中两个常用属性:

  1. widget,它表示与该 State 实例关联的 Widget实例 。 需要注意的是,这种关联不是永久的,因为 State 的实例只有在第一次插入树中会被创建, 而 StatefulWidget 因为改变,其实例会被多次创建, 那么 State.widget 就会被动态设置为新的 Widget
  2. context, 就是 BuildContext

3.4.1 State 的生命周期

State 的生命周期对理解 Flutter 是非常重要的。下面来通过官方的例子来学习 State 的生命周期。

实现一个计数器的功能 CounterWidget 组件, 点击可以使得计数+1,由于要保存计数器的数值状态,所以我们应继承 StatefulWidget,代码如下:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  final int initValue;

  @override
  State<StatefulWidget> createState() => _CounterWidgetState();
}

CounterWidget 接受一个 initValue 的整型,它表示计数器的初始值,而 createState() 方法则创建一个 CounterWidgetState 的 State,用于绑定该 Widget ,来看下 State 的代码:

class CounterWidget extends StatefulWidget {
  const CounterWidget({Key? key, this.initValue = 0});

  final int initValue;

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _counter = widget.initValue;
    print("init State :$_counter");
  }

  @override
  Widget build(BuildContext context) {
    print("build");
    return Scaffold(
      body: Center(
        child: TextButton(
          child: Text("$_counter"),
          // 点击事件, 点击后自增
          onPressed: () =>
              setState(() {
                ++_counter;
              }),
        ),
      ),
    );
  }
  
  @override
  void didUpdateWidget(covariant CounterWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    print("didUpdateWidget");
  }
  
  @override
  void deactivate() {
    super.deactivate();
    print("deactivate");
  }
  

  @override
  void dispose() {
    super.dispose();
    print("dispose");
  }
  
  @override
  void reassemble() {
    super.reassemble();
    print("reassemble");
  }
  
  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    print("didChangeDependencies");
  }
}

接下来使用初始页来打开一个新路由,在新路由里面只显示这个 Widget,新打开页面后,日志会输出:

在这里插入图片描述
在 StatefulWidget 插入到 Widget 树时, State 的 initState() 会被调用

然后点击 ⚡️ 按钮热重载,控制台会输出下面的日志:

reassemble
deactive
dispose

在 Counter 从 Widget 树中移除时, deactviedispose 会被依次调用,下面来看看各个回调函数:

StatefulWidget 的生命周期图如下所示:
在这里插入图片描述

3.4.2 build 方法为什么在 State 中而不是在 StatefulWidget 中

前面介绍过, StatelessWidget 中是有 build() 方法中,但与之对应的 StatefulWidget 却把 build() 方法放在了 State中,这是为什么呢?

这主要是为了提高开发的灵活性,如果将 build() 放在 StatefulWidget 主要有两个问题:

  1. 状态访问不便
    假如我们的 StatefulWidget 有很多的状态,而每次状态改变都要调用 build(),由于状态是放在 State 中的,那么 build 和 State 放在两个类别中,构建时读取状态会很不方便。
    并且需要把 State 设置为公开状态,这会导致状态不再具有私密性,导致其修改会不可控。
  2. 继承 StatefulWidget 不便
    子类继承 StatefulWidget 类,意味着要做状态传递,做状态传递是毫无意义的,具体可以参考:为什么不将 build 方法放在StatefulWidget上

3.4.3 在 Widget 树中获取 State 对象

StatefulWidget 的逻辑都都是在其 State 中,所以很多时候,需要获取 StatefulWidget.State 对象来调用一些方法是,比如 Scaffold 组件打开 SnackBar 的逻辑就是放在其 State:ScaffoldState 中的。

我们有两种方法在 子 Widget 树中获取 父级 StatefulWidget 的State 对象。

3.4.3.1 通过 Context获取

有一个 context.findAncestorStateOfType() 方法,该方法可以从当前节点沿着 Widget 树向上查找指定类型的 StatefulWidget 对应的 State 对象,下面是实现打开 SnackBar 的示例:

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  @override
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取State对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单1'),
              );}),],),),
      drawer: Drawer(),
    );
  }
}

一般来说, 如果 StatefulWidget 的状态是私有的,那么就不应该去直接获取其 State 的对象,因为其不希望被暴露出来。
相反的,如果 StatefulWidget 的状态是暴露出来的,我们就可以去获取。

但通过 context.findAncestorStateOfType() 获取 StetefulWidget 的状态的方法是通用的,我们并不能在语法层面上指定 StatefulWidget 的状态是否为私有。

所以在Flutter开发中有一个潜规则:如果 StatefulWidget 的状态是希望暴露出来的,应该在 StatefulWidget 中提供一个 of() 的静态方法来获取其 State 对象,开发者可以直接通过该方法来获取,如果不希望暴露,则不提供该方法

Scaffold 也提供了一个 of 方法,我们可以直接调用它:

Builder(builder: (context) {
  return ElevatedButton(
    onPressed: () {
      // 直接通过of静态方法来获取ScaffoldState
      ScaffoldState _state=Scaffold.of(context);
      // 打开抽屉菜单
      _state.openDrawer();
    },
    child: Text('打开抽屉菜单2'),
  );
}),

3.4.3.2 通过 GlobalKey 获取

通过 GlobalKey 来获取也是一个常用的方式,步骤为:

  1. 给目标 StatefulWidget 添加 GlobalKey:
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
    key: _globalKey , //设置key
    ...  
)
  1. 通过 GlobalKey 来获取 State 对象
_globalKey.currentState.openDrawer()

GlobalKey 其实是 FLutter 提供的一种整个 App 中应用 element 的机制, 如果一个 Widget 设置了 GlobalKey, 我们可以通过

3.5 通过 RenderObject 自定义 Widget

StatelessWidget 和 StatefulWidget 都是用于组合组件的, 他们本身没有对应的 RenderObject

Flutter 库中很多基础组件都不是通过 StatelessWidget 和 StatefulWidget 实现的, 例如 Text、Colume、Align。他们都是积木,“元组件”,而这些元组件都是通过自定义 RenderObject 来实现的

实际上 Flutter 最原始定义组件的方式就是通过定义 RnederObject 来实现, 用官方示例来简单演示一下通过 RenderObject 定义组件的方式:

class CustomWidget extends LeafRenderObjectWidget{
  @override
  RenderObject createRenderObject(BuildContext context) {
    // 创建 RenderObject
    return RenderCustomObject();
  }
  @override
  void updateRenderObject(BuildContext context, RenderCustomObject  renderObject) {
    // 更新 RenderObject
    super.updateRenderObject(context, renderObject);
  }
}

class RenderCustomObject extends RenderBox{

  @override
  void performLayout() {
    // 实现布局逻辑
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 实现绘制
  }
}

如果组件不会包含子组件,则可以直接继承 LeafRenderObjectWidget, 它是 RenderObjectWidget 的子类,而 RenderObjectWidget 继承 子Widget,如下所示:

abstract class LeafRenderObjectWidget extends RenderObjectWidget {
  const LeafRenderObjectWidget({ Key? key }) : super(key: key);

  @override
  LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}

它返回的 Element 是一个 LeafRenderObjectElement,如果自定义的 Widget 可以包含子组件,则可以根据子组件的数量来选择继承 SingleChildRenderObject 或者 MultiChildRenderObjectWidget

RenderCustomObject 类是继承 RenderBox, 而 RnederBox 继承自 RenderObject我们需要在 RenderCustomObject 中实现布局、绘制、事件响应等逻辑,关于如何实现这些逻辑,以后会讲到。

参考

官方文档
Flutter基础四

标签:Widget,简介,StatefulWidget,override,State,组件,Flutter
来源: https://blog.csdn.net/rikkatheworld/article/details/121280618