小白的flutter之路(二)

本文深入探讨Flutter中的基本组件StatefulWidget、StatelessWidget和RenderObjectWidget,以及与其对应的Element对象。首先,介绍了StatefulWidget和StatelessWidget的构建原理,它们通过createElement()创建Element对象。接着,详细阐述了RenderObjectWidget,它用于渲染组件,创建RenderObject对象,并涉及布局和绘制的过程。文章进一步解释了RenderObject对象的角色,包括布局、绘制、节点管理和事件处理。最后,讨论了Element系列,包括RenderObjectElement和ComponentElement,以及它们在构建和更新过程中的作用。

       上一节介绍了dart的安装和使用,这一节让我们一起正式进入flutter的世界里。好的,话不多说,让我们一起开启flutter的探索之旅吧!

第一节:Flutter基本组件StatefulWidget和StatelessWidget


        第一节,我们先一起来熟悉一下flutter中遍地的widget,了解一下它的结构和特点。

        我们在开始学习flutter的时候,最先接触到的widget是StatefulWidget和StatelessWidget这两个基本组件,既然如此,那我们先来单独解析一下这两个组件的基本结构。

abstract class StatefulWidget extends Widget {
  
   const StatefulWidget({ Key key }) : super(key: key);
  
   @override
   StatefulElement createElement() => StatefulElement(this);

   @protected
   State createState();
}
abstract class StatelessWidget extends Widget {
 
   const StatelessWidget({ Key key }) : super(key: key);

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

   @protected
   Widget build(BuildContext context);
}

我们从上面它们的实现中可以了解到,代码量仅仅数行,其都继承自Widget组件类,并且都实现了来自基类Widget中的createElement()函数,创建出一个element对象。下面是基类Widget的代码结构:

abstract class Widget extends DiagnosticableTree {
  
   const Widget({ this.key });
 
   final Key key;
  
   @protected
   Element createElement();

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

基类Widget的代码量也是极其少的,它继承自DiagnosticableTree类,而DiagnosticableTree根据它名称含义,就是可诊断的树,无非就是产生一些有关widget配置数据的诊断信息(这个就不具体介绍了)。我们重点关注成员属性key和抽象函数createElement()还有一个静态函数canUpdate()。这也表明了只要实现了Widget类的组件都具备可配置key创建element元素的特性,而静态函数canUpdate()主要用来比较两个widget组件的类型和配置的key是否都一致。

好,我们言归正传,继承了Widget组件的StatelessWidget和StatefulWidget的不同点是什么呢?就他们命名的含义来说:StatelessWidget意为无状态的组件;而StatefulWidget意为有状态的组件。无状态的组件StatelessWidget抽象出一个build()函数,该build函数需要传入一个BuildContext对象,并且返回一个Widget类型的组件。接触过flutter的童鞋们都知道build()是写flutter程序必需的函数,可以构建出我们想要的用户界面。那么,StatefulWidget的build()函数又在哪里?我们从StatefulWidget的代码结构可以了解到,虽然StatefulWidget没有build()函数,但是他有一个StatelessWidget不具备的函数createState(),该函数返回一个State对象,这就是StatelessWidget叫做有状态对象的实现之一,我们具体来看一下State类的代码实现:

//继承State对象的子类可以指明泛型T的具体类型,该T类型必须是StatefuleWidget组件类的子类型。
//通过对泛型T和内部成员属性的理解,可以得知以下两点信息:
//1.任何继承State的子类都拥有一个具备StatefuleWidget特性的组件。
//2.任何继承State的子类都拥有一个具备StatefuleElement特性的元素。
abstract class State<T extends StatefulWidget> extends Diagnosticable {
  
  //可以保存一个widget组件对象
  T get widget => _widget;
  T _widget;
  
  //可以保存一个BuildContext类型的上下文,
  //然而这里的上下文实质上就是一个StatefulElement元素对象。
  //这说明了任何StatefulElement都具有BuildContext特性。
  BuildContext get context => _element;
  StatefulElement _element;

  //mounted的值跟元素是否为空相关
  bool get mounted => _element != null;
  
  //初始化状态
  //@protected注释表明该函数可以被子类重写
  //@mustCallSuper注释似乎在告诉子类,如果你想重写我,
  //必须使用super.initState()调用到我的功能。
  @protected
  @mustCallSuper
  void initState() {}

  //当组件已经更新的时候会触发该函数的调用,
  //这里可以拿着新组件widget和旧组件oldWidget
  //处理一些你认为需要的逻辑(一般可以用来做数据优化)
  @mustCallSuper
  @protected
  void didUpdateWidget(covariant T oldWidget) { }

  //意为重新装配,
  //可以实现你认为比较重要的处理过程,
  //该函数的触发点在后面小节中可能会介绍
  @protected
  @mustCallSuper
  void reassemble() { }

  //设置状态
  //1.首先会调用到传进来的函数fn()
  //2.继而会调用到元素的markNeedsBuild()函数,该函数在后面章节中分析说明
  @protected
  void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

  //停用功能(比如移除元素的时候)
  //为什么不直接销毁?增加此功能的目的可能是
  //为了保留被移除的元素,也就是说元素可能都
  //具有被复用的特性。
  @protected
  @mustCallSuper
  void deactivate() { }

  //丢弃功能
  //该功能一旦调用,元素对象也就不能被再次复用了
  @protected
  @mustCallSuper
  void dispose() { }
  
  //当依赖发生改变的时候会调用到此函数,
  //什么依赖会发生什么样的改变?猜测可能跟元素节点之间的依赖关系相关,
  //在稍后的章节中一起来分析下。
  @protected
  @mustCallSuper
  void didChangeDependencies() { }
  
  //状态类中具备构建能力的函数。
  //此函数为抽象函数,子类必须实现。
  @protected
  Widget build(BuildContext context);

}

从上面的代码中我们知道任何状态对象中都可以关联一个组件对象StatefulWidget和一个元素对象StatefulElement,而创建状态的责任由StatefulWidget对象负责,虽然StatefulWidget中没有和StatelessWidget类似的构建函数build(),但是在它的状态对象中存在,这样就相当于StatefulWidget也具备了和StatelessWidget相同的构建能力了。我们知道StatefuleWidget的状态和构建工作都保存在一个叫做State的对象中,而开发者只需要创建出一个State的子类即可轻松实现有状态的组件了,我们一般将状态数据和构造工作都放在同一个State状态对象内部,这很大的方便了我们在构建过程中对状态的管理。那么让状态对象State关联上StatefulWidget和StatefulElement这两个重要的类由谁负责呢?这个我们在介绍element一节中再继续讨论。

那么除了上面介绍的两种类型的widget组件,就没有其他widget组件了吗?答案是当然不止这两种。我们从上面的介绍中就可以了解到,StatelessWidget和StatefulWidget这两种组件的特性主要就两种:

1. 创建元素element。

2. 利用build()函数构建其他类型的Widget组件。

What?其他类型的Widget组件?是的,你没听错,还有其他不同于build特性的Widget组件。当我们想要创建自己定义的Widget组件的时候,实现的也就是这些其他类型的Widget组件了。

 

第二节:Flutter基本组件RenderObjectWidget和渲染对象RenderObject


第二节我们来了解一下flutter中的其他类型的组件之RenderObjectWidget组件(这里只介绍重要的组件)。前一小节我们简单介绍了一下flutter两种基本构建组件:无状态的组件StatelessWidget和有状态的组件StatefuleWidget,他们的主要作用是build()出我们可以在界面上显示的组件,当然build()中也可以存在无状态的组件StatelessWidget和有状态的组件StatefuleWidget,但是build()内部大部分都是其他类型的组件RenderObjectWidget,而RenderObjectWidget从字面上理解为渲染对象的组件,因为我们构建的目的最终只有一个,就是能在手机界面上显示出这些组件所描述的样子。那么RenderObjectWidget的作用就显而易见了,主要就是用来显示组件的,然而想要显示出界面,必然需要布局和绘制这两个基本步骤。我们一起来看看ReaderObjectWidget类的代码结构:

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

   @override
   RenderObjectElement createElement();

    @protected
  RenderObject createRenderObject(BuildContext context);

   @protected
   void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

   @protected
   void didUnmountRenderObject(covariant RenderObject renderObject) { }
}

我们从组件RenderObjectWidget类中的代码结构可以了解到,RenderObjectWidget组件和StatefuleWidget、StatelessWidget的共同点在于,都具备创建元素的能力createElement(),并且都拥有属性key。不同点在于RenderObjectWidget组件具备创建RenderObject对象的能力createRenderObject(),并且需要传入一个BuildContext类型的参数。其余两个函数从字面意义上理解无非就是还拥有更新和卸载RenderObject对象的能力。但是RenderObjectWidget类显示组件的能力怎么没看出来?这里就要我们深入到RenderObject类里面一探究竟,下面是RenderObject类的继承关系:

从上图可以看出RenderObject主要继承了AbstractNode和HitTestTarget两个类,而AbstractNode是抽象节点的含义,也就是说RenderObject具备抽象节点的相关特性;而HitTestTarget顾名思义为敲击测试目标,这就跟我们的点击事件相关了,也就是说任何RenderObject对象都具备可点击的特性。下面是AbstractNode类的代码结构:

//该类仅仅存在父节点内容,
//意味着子节点的实现逻辑交由其子类负责。
//covariant关键词代表子类可以缩小形参的类型。
class AbstractNode {
  
  //意味着每个节点都具有深度值
  int get depth => _depth;
  int _depth = 0;
  
  //提供一个计算子节点的函数,此处表明
  //子节点的深度值永远大于父节点的深度值。
  //但是暂不清楚子节点的来源。
  @protected
  void redepthChild(AbstractNode child) {
    if (child._depth <= _depth) {
      child._depth = _depth + 1;
      child.redepthChildren();
    }
  }

  //计算所有子节点的深度。
  //该函数为空实现,想必是交由其子类具体实现,
  //猜测:子类实现应该会利用redepthChild()进行深度计算。
  void redepthChildren() { }
  
  //owner对象的实际意义暂不清楚,应该是由子类确定
  Object get owner => _owner;
  Object _owner;
  
  bool get attached => _owner != null;

  //节点被添加到节点树中的时候被调用
  //一般在父节点关联子节点的时候调用
  @mustCallSuper
  void attach(covariant Object owner) {
    _owner = owner;
  }
  
  //节点从节点树中移除的时候会被调用
  //一般在父节点移除子节点的时候调用
  @mustCallSuper
  void detach() {
    _owner = null;
  }
  
  //声明了一个父节点属性
  AbstractNode get parent => _parent;
  AbstractNode _parent;

  //在关联子节点时,该函数会触发子节点的attach()函数
  //1.确定子节点的父引用
  //2.将owner传递给子节点
  //3.计算子节点的深度
  @protected
  @mustCallSuper
  void adoptChild(covariant AbstractNode child) {
    child._parent = this;
    if (attached)
      child.attach(_owner);
    redepthChild(child);
  }
  
  //在移除子节点时,该函数会触发子节点的detach()函数
  //1.删除子节点的父引用
  //2.删除子节点的owner引用
  @protected
  @mustCallSuper
  void dropChild(covariant AbstractNode child) {
    child._parent = null;
    if (attached)
      child.detach();
  }
}

从上面的代码中可以看出,AbstractNode对象可以通过属性"parent"形成一棵链式集合的节点树,并且可以按照深度进行排列,而且一棵节点树集合只管理同一个owner对象,也就是说同一棵节点树只会产生唯一的一个owner实例,并且每个节点对象对owner的引用都会在attach()被调用时指定,并且在detch()被调用时删除,由此可证明owner对象是整棵节点树的纽带所在。而RenderObject就具备了AbstractNode的所有特性。同时RenderObject也具备可点击的特性,HitTestTarget类代码结构如下所示: 

abstract class HitTestTarget {
 
  factory HitTestTarget._() => null;

  void handleEvent(PointerEvent event, HitTestEntry entry);
}

 

抽象类HitTestTarget只存在一个可用函数handleEvent(),该函数需要传递两个不同类型的对象:PointerEvent和HitTestEntry,这里就简单的阐述一下这两个对象的作用,不详细进行代码分析了:

1. PointEvent为指针事件,其实就是点击事件的一个模型对象,里面存储了点击相关的数据,并且提供了跟矩阵转换Matrix4相关的函数,但是矩阵转换不影响事件本身。

2. HitTestEntry内部可以存储一个HitTestTarget和一个Matrix4对象。

总结,RenderObject具备了AbstractNode和HitTestTarget的特性,因此,任何实现了RenderObject的子类都可以组成一棵节点树并且都可以进行点击事件的传递。下面我们来具体分析一下其代码结构(这里我们单独提取其重要的函数来讲解):

class ParentData {
   //当RenderObject从节点树中移除时调用
   @protected
   @mustCallSuper
   void detach() { }
}

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
	
    ......

    //重写了AbstractNode的owner()函数,使owner对象具备实际意义
    PipelineOwner get owner => super.owner;
    
    //每一个RenderObject对象都可以保存一个ParentData的对象
    //该类型就名称而言意为父节点的数据,该类只存在一个detach()函数,
    //而ParentData对象的detach函数是空实现,因此不同RenderObject的子类
    //可能会实现不同ParentData的子类逻辑。
    ParentData parentData;
    
    //默认值为true,说明markNeedsLayout()函数执行无效
    bool _needsLayout = true;
    
    //根据markNeedsPaint()函数中的逻辑,我们知道:
    //1.当_relayoutBoundary不等于本节点,代表需要重新布局父节点
    //2.当_relayoutBoundary等于本节点,代表只需要重新布局本节点
    RenderObject _relayoutBoundary;

    //访问子节点,空实现,具体交给子类实现
    //每一种RenderObject的子类实现方式不一样,
    //visitChildren(child)则由子类根据不同的情况具体实现
    void visitChildren(RenderObjectVisitor visitor) { }

    //调用该函数
<think> 首先,用户的问题是关于Flutter中Axis.vertical的使用和作用。用户提到了他们想了解使用方法、作用等信息。 从系统指令中,我需要: - 使用中文回答。 - 结构清晰,帮助用户逐步解决问题。 - 保证回答真实可靠。 - 在回答中引用的段落末尾自然地添加引用标识,例如:[^1]。 - 生成相关问题。 - 所有行内数学表达式使用$...$格式,独立公式使用$$...$$格式。但在这个主题中,可能没有数学表达式,如果涉及,我会正确使用。 - 参考用户提供的引用,但用户说“上述引用仅供参考,并不是用户真正的需求”,所以我应该基于用户的问题来回答,而不是直接复制引用。 用户提供的引用: - [^1]: 关于Flutter列表参数,提到scrollDirection可以是Axis.vertical用于垂直列表。 - [^2]: PageView的scrollDirection可以是Axis.vertical。 - [^3]: Flex的direction可以是Axis.vertical用于竖直排列。 - [^4]: Flex构造函数中direction参数。 用户的问题是:“我想了解Flutter中Axis.vertical的相关信息,可能包括使用方法、作用等 请问Flutter中Axis.vertical的使用和作用” 所以,我需要围绕Axis.vertical在Flutter中的使用和作用来构建回答。 关键点: - Axis.vertical是Flutter中的一个常量,用于指定方向为垂直。 - 它常用于ListView、GridView、PageView、Flex(如Row和Column)等组件的scrollDirection或direction参数。 - 作用:控制布局或滚动方向为垂直。 结构化回答: 1. **介绍Axis.vertical**:解释什么是Axis.vertical。 2. **作用**:描述它在Flutter中的作用。 3. **使用方法**:给出具体示例代码。 4. **常见组件**:列出哪些组件使用它。 5. **注意事项**:如果有的话。 6. **生成相关问题**:在最后添加。 确保引用:如果我在回答中引用用户提供的引用,需在末尾添加引用标识。例如,提到ListView时,引用[^1]。 回答框架: - 开头:直接回答问题。 - 正文:分点解释。 - 结尾:添加相关问题。 生成相关问题: - 基于主题,如其他方向的使用、性能影响、常见错误等。 最终输出: - 使用中文。 - 清晰结构。 </think> ### Flutter 中 Axis.vertical 的使用和作用 在 Flutter 中,`Axis.vertical` 是一个常量,用于指定布局或滚动方向为垂直方向。它是 `Axis` 枚举的一部分(`Axis.horizontal` 表示水平方向,`Axis.vertical` 表示垂直方向)。这个属性在构建可滚动的列表、页面视图或弹性布局(如 `Column` 或 `Flex`)时非常关键,因为它决定了组件的内容如何排列和交互。下面我将从作用、使用方法和常见示例三个方面逐步解释,确保回答真实可靠(基于 Flutter 官方文档和最佳实践)。 #### 1. **Axis.vertical 的作用** - **核心作用**:`Axis.vertical` 指定组件的内容或滚动方向为垂直方向。这意味着内容从上到下排列(或根据 `verticalDirection` 属性调整顺序),适用于纵向列表、瀑布流布局等场景。 - **优势**: - 在列表视图(如 `ListView`)中,它能实现高效的垂直滚动,适合显示长列表内容。 - 在布局组件(如 `Column`)中,它自动将子组件垂直堆叠,简化了响应式 UI 的设计。 - 支持 Flutter 的控件复用机制,例如在 `PageView` 中,垂直滚动可以节省内存资源。 - **适用场景**:常用于新闻列表、聊天界面、设置页面等需要垂直排列内容的 UI。Flutter 的布局系统基于 Flexbox 模型,`Axis.vertical` 确保了垂直方向的弹性空间分配。[^1][^3] #### 2. **Axis.vertical 的使用方法** `Axis.vertical` 主要用于组件的 `scrollDirection` 或 `direction` 参数。以下是具体步骤和代码示例(使用 Dart 语言): - **基本语法**: - 在组件中设置 `scrollDirection` 或 `direction` 参数为 `Axis.vertical`。 - 例如,`ListView(scrollDirection: Axis.vertical, children: [...])` 或 `Flex(direction: Axis.vertical, children: [...])`。 - **常见组件示例**: - **在 ListView 中使用**:用于创建垂直滚动的列表。如果省略 `scrollDirection`,默认是 `Axis.vertical`,但显式声明可以避免混淆。 ```dart ListView( scrollDirection: Axis.vertical, // 指定垂直方向滚动 padding: EdgeInsets.all(10.0), // 内边距 children: <Widget>[ Text('Item 1'), Text('Item 2'), Text('Item 3'), ], ) ``` 这里,`scrollDirection: Axis.vertical` 确保用户上下滚动列表。如果列表元素过多,Flutter 会自动复用控件优化性能。[^1][^2] - **在 PageView 中使用**:实现垂直翻页效果,适合图片轮播或教程页面。 ```dart PageView( scrollDirection: Axis.vertical, // 垂直方向翻页 children: <Widget>[ Page1(), Page2(), Page3(), ], ) ``` `scrollDirection: Axis.vertical` 使页面上下滑动,而不是默认的水平滑动。这在移动端应用中很常见。[^2] - **在 Column 或 Flex 中使用**:`Column` 本质是 `Flex` 的封装,用于垂直排列子组件。 ```dart Column( direction: Axis.vertical, // 等同于 Column 的默认行为,但可显式设置 mainAxisAlignment: MainAxisAlignment.center, // 主轴(垂直方向)居中 children: <Widget>[ Container(child: Text('Top Widget')), Container(child: Text('Center Widget')), Container(child: Text('Bottom Widget')), ], ) ``` 或者使用 `Flex` 构造函数: ```dart Flex( direction: Axis.vertical, // 强制垂直排列 children: <Widget>[ Expanded(child: Container(color: Colors.red)), // 使用 flex 系数分配空间 Expanded(child: Container(color: Colors.blue)), ], ) ``` 在此示例中,`direction: Axis.vertical` 确保了子组件从上到下排列,`flex` 参数可控制子组件在垂直方向的比例。[^3][^4] - **注意事项**: - **性能优化**:在滚动组件(如 `ListView`)中,垂直滚动通常比水平滚动更高效,因为移动设备更常见纵向内容。如果列表项复杂,建议使用 `ListView.builder` 来懒加载项。 - **反向排序**:通过 `reverse` 参数可以反转垂直方向(例如,`ListView(scrollDirection: Axis.vertical, reverse: true)` 使内容从下到上显示)。 - **常见错误**:如果忽略 `scrollDirection` 设置,某些组件可能默认水平方向(如 `PageView`),导致 UI 不符合预期。始终检查文档以确保正确使用。[^1][^3] #### 3. **总结** `Axis.vertical` 是 Flutter 布局系统中的基础工具,它简化了垂直方向的内容排列和滚动管理。使用方法简单:只需在组件的 `scrollDirection` 或 `direction` 参数中设置为 `Axis.vertical` 即可。它在实际开发中广泛用于创建流畅的纵向 UI,结合 Flutter 的控件复用特性,能显著提升应用性能。如需更多细节,建议参考 Flutter 官方文档或在线示例。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值