文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

详解Flutter Widget

2024-04-02 19:55

关注

 概述:

所有的一切都可以被称为widget

在开发 Flutter 应用过程中,接触最多的无疑就是Widget,是『描述』 Flutter UI 的基本单元,通过Widget可以做到:

Google 在设计Widget时,还赋予它一些鲜明的特点:

Widget的本质:

在Widget源码中有这样一段注释:

请添加图片描述

这段注释阐明了Widget的本质:用于配置Element的,Widget本质上是 UI 的配置信息 (附带部分业务逻辑)。

我们通常会将通过Widget描述的 UI 层级结构称之为「Widget Tree」,但与「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比,实质上并不存在「Widget Tree」。为了描述方便,将 Widget 组合描述的 UI 层级结构称之为「Widget Tree」,也未尝不可。

分类:

请添加图片描述

Widget

Widget,所有 Widget 的基类。

请添加图片描述

如上图所示,在 Widget基类中有 3 个重要的方法 (属性):

GlobalKey 是一类较特殊的 key,在介绍 Element 时会附带介绍。

上述更新流程,同样在介绍 Element 时会重点分析。

StatelessWidget

无状态-组合型 Widget,由其build方法描述组合 UI 的层级结构。在其生命周期内状态不可变。


/// A widget that does not require mutable state.
///
/// A stateless widget is a widget that describes part of the user interface by
/// building a constellation of other widgets that describe the user interface
/// more concretely. The building process continues recursively until the
/// description of the user interface is fully concrete (e.g., consists
/// entirely of [RenderObjectWidget]s, which describe concrete [RenderObject]s).

具体是两个方法:


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

@protected
Widget build(BuildContext context);

以『声明式 UI』的形式描述了该组合式 Widget 的 UI 层级结构及样式信息,也是开发 Flutter 应用的主要工作『场所』。该方法在 3 种情况下被调用:

当「Parent Widget」或 依赖的「Inherited Widget」频繁变化时,build方法也会频繁被调用。因此,提升build方法的性能就显得十分重要,Flutter 官方给出了几点建议:

关于 const constructor 可以看看我这篇文章。

StatefulWidget

有状态-组合型 Widget,但要注意的是StatefulWidget本身还是不可变的,其可变状态存在于State中。


/// A widget that has mutable state.
///
/// State is information that (1) can be read synchronously when the widget is
/// built and (2) might change during the lifetime of the widget. It is the
/// responsibility of the widget implementer to ensure that the [State] is
/// promptly notified when such state changes, using [State.setState].

具体有两个方法:


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

@protected
@factory
State createState(); // ignore: no_logic_in_create_state, this is the original sin

StatefulElement(StatefulWidget widget)
     : _state = widget.createState(),
       super(widget) {
   _state._element = this;
   _state._widget = widget;
 }

实际上是「Stateful Widget」对应的「Stateful Element」被添加到 Element Tree 时,伴随「Stateful Element」的初始化,createState方法被调用。从后文可知一个 Widget 实例可以对应多个 Element 实例 (也就是同一份配置信息 (Widget) 可以在 Element Tree 上不同位置配置多个 Element 节点),因此,createState方法在「Stateful Widget」生命周期内可能会被调用多次。
另外,需要注意的是配有GlobalKey的 Widget 对应的 Element 在整个 Element Tree 中只有一个实例。

State

有状态小部件 的逻辑和Stateful Widget

State 用于处理「Stateful Widget」的业务逻辑以及可变状态
由于其内部状态是可变的,故 State 有较复杂的生命周期:

请添加图片描述

如上图,State 的生命周期大致可以分为 8 个阶段:

StatefulElement.constructor中的_state._element = this;可知,State._emelent指向了对应的 Element 实例,而我们熟知的State.context引用的就是这个_elementBuildContext get context => _element;
State实例与Element实例间的绑定关系一经确定,在整个生命周期内不会再变了 (Element 对应的 Widget 可能会变,但对应的 State 永远不会变),期间,Element可以在树上移动,但上述关系不会变 (即「Stateful Element」是带着状态移动的)。

上述 3 棵树以及更新流程在后续文章中会有详细介绍

重新插入操作必须在当前帧动画结束之前

请添加图片描述

至此,State 中的核心方法基本都已在上述过程中介绍了,下面重点看一下setState方法:


void setState(VoidCallback fn) {
  assert(fn != null);
  assert(() {
    if (_debugLifecycleState == _StateLifecycle.defunct) {
      throw FlutterError.fromParts(<DiagnosticsNode>[...]);
    }
    if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
      throw FlutterError.fromParts(<DiagnosticsNode>[...]);
    }
    return true;
  }());
  final dynamic result = fn() as dynamic;
  assert(() {
    if (result is Future) {
      throw FlutterError.fromParts(<DiagnosticsNode>[...]);
    }
    return true;
  }());
  _element.markNeedsBuild();
}

关于 State 最后再强调 2 点:

等 3 方法间有正确的订阅 (subscribe) 与取消订阅 (unsubscribe) 的操作:

initState中执行 subscribe;

如果关联的「Stateful Widget」与订阅有关,在didUpdateWidget中先取消旧的订阅,再执行新的订阅;

dispose中执行 unsubscribe。

ParentDataWidget

ParentDataWidget以及下面要介绍的InheritedElement都继承自ProxyWidget,由于ProxyWidget作为抽象基类本身没有任何功能,故下面直接介绍ParentDataWidgetInheritedElement


/// Base class for widgets that hook [ParentData] information to children of/// [RenderObjectWidget]s.

ParentDataWidget作为 Proxy 型 Widget,其功能主要是为其他 Widget 提供ParentData信息。虽然其 child widget 不一定是 RenderObejctWidget 类型,但其提供的ParentData信息最终都会落地到 RenderObejctWidget 类型子孙 Widget 上。

ParentData 是『parent renderobject』在 layout『child renderobject』时使用的辅助定位信息,详细信息会在介绍 RenderObject 时介绍。


void attachRenderObject(dynamic newSlot) {
	assert(_ancestorRenderObjectElement == null);
	_slot = newSlot;
	_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
	_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
	final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
	if (parentDataElement != null)  
	                                          _updateParentData(parentDataElement.widget);
}
ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() {
	Element ancestor = _parent;
	while (ancestor != null && ancestor is! RenderObjectElement) 
	                                          {
		if (ancestor is ParentDataElement<RenderObjectWidget>) 
		                                          return ancestor;
		ancestor = ancestor._parent;
	}
	return null;
}
void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) 
                                          {
	parentData.applyParentData(renderObject);
}

上面这段代码来自RenderObjectElement,可以看到在其attachRenderObject方法第 6 行从祖先节点找ParentDataElement,如果找到就用其 Widget(ParentDataWidget) 中的 parentData 信息去设置 Render Obejct。在查找过程中如查到RenderObjectElement (第 13 行),说明当前 RenderObject 没有 Parent Data 信息。
最终会调用到ParentDataWidget.applyParentData(RenderObject renderObject),子类需要重写该方法,以便设置对应RenderObject.parentData

来看个例子,通常配合Stack使用的Positioned(继承自ParentDataWidget):


void applyParentData(RenderObject renderObject) {
	assert(renderObject.parentData is StackParentData);
	final StackParentData parentData = renderObject.parentData;
	bool needsLayout = false;
	if (parentData.left != left)
	  {
		parentData.left = left;
		needsLayout = true;
	}
	...  if (parentData.width != width) 
	  {
		parentData.width = width;
		needsLayout = true;
	}
	...  if (needsLayout) {
		final AbstractNode targetParent = renderObject.parent;
		if (targetParent is RenderObject)    
		      targetParent.markNeedsLayout();
	}
}

可以看到,Positioned在必要时将自己的属性赋值给了对应的RenderObject.parentData (此处是StackParentData),并对「parent render object」调用markNeedsLayout(第 19 行),以便重新 layout,毕竟修改了布局相关的信息。


abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidget

如上所示,ParentDataWidget在定义上使用了泛型<T extends RenderObjectWidget>,其背后的含义是:
从当前ParentDataWidget节点向上追溯形成的祖先节点链(『parent widget chain』)上,在 2 个ParentDataWidget类型的节点形成的链上至少要有一个『RenderObject Widget』类型的节点。因为一个『RenderObject Widget』不能接受来自 2 个及以上『ParentData Widget』的信息。


/// Base class for widgets that efficiently propagate information down the tree.
// To obtain the nearest instance of a particular type of inherited widget from
///a build context, use [BuildContext.dependOnInheritedWidgetOfExactType].

InheritedWidget 用于在树上向下传递数据。
通过BuildContext.dependOnInheritedWidgetOfExactType可以获取最近的「Inherited Widget」,需要注意的是通过这种方式获取「Inherited Widget」时,当「Inherited Widget」状态有变化时,会导致该引用方 rebuild。

具体原理在介绍 Element 时会详细分析。

通常,为了使用方便会「Inherited Widget」会提供静态方法of,在该方法中调用BuildContext.dependOnInheritedWidgetOfExactTypeof方法可以直接返回「Inherited Widget」,也可以是具体的数据。

有时,「Inherited Widget」是作为另一个类的实现细节而存在的,其本身是私有的(外部不可见),此时of方法就会放到对外公开的类上。最典型的例子就是Theme,其本身是StatelessWidget类型,但其内部创建了一个「Inherited Widget」:_InheritedThemeof方法就定义在上Theme上:


static MediaQueryData of(BuildContext context, {
	bool nullOk = false })
	{
		final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
		if (query != null)    
		  return query.data;
		if (nullOk)    return null;
	}

of方法返回的是ThemeData类型的具体数据,并在其内部首先调用了BuildContext.dependOnInheritedWidgetOfExactType

我们经常使用的「Inherited Widget」莫过于MediaQuery,同样提供了of方法:

请添加图片描述

如下是MediaQuery.updateShouldNotify的实现,在新老Widget.data 不相等时才 rebuilt 那依赖的 Widget。


bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;

RenderObjectWidget

真正与渲染相关的 Widget,属于最核心的类型,一切其他类型的 Widget 要渲染到屏幕上,最终都要回归到该类型的 Widget 上。


@overrideRenderFlex createRenderObject(BuildContext context) {
	return RenderFlex(  
	  direction: direction,    
	  mainAxisAlignment: mainAxisAlignment,    
	  mainAxisSize: mainAxisSize,    
	  crossAxisAlignment: crossAxisAlignment,   
	  textDirection: getEffectiveTextDirection(context),    
	  verticalDirection: verticalDirection,    
	    textBaseline: textBaseline,  
	  )
	;
}

上面是Flex.createRenderObject的源码,真实感受一下 (还是代码更有感觉)。可以看到,用Flex的信息(配置)初始化了RenderFlex

FlexRowColumn的基类,RenderFlex继承自RenderBox,后者继续自RenderObject


@overridevoid updateRenderObject(BuildContext context, covariant RenderFlex renderObject) 
{
	renderObject   
	  ..
	direction = direction   
	  ..mainAxisAlignment = mainAxisAlignment   
	  ..
	mainAxisSize = mainAxisSize   
	  ..
	crossAxisAlignment = crossAxisAlignment  
	  ..
	textDirection = getEffectiveTextDirection(context) 
	  ..
	verticalDirection = verticalDirection   
	  ..
	textBaseline = textBaseline;
}

Flex.updateRenderObject的源码也很简单,与Flex.createRenderObject几乎一一对应,用当前Flex的信息修改renderObject

RenderObjectWidget的几个子类:LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidget只是重写了createElement方法以便返回各自对应的具体的 Element 类实例。

小结


至此,重要的基础型 Widget 基本介绍完了,总结一下:

到此这篇关于详解Flutter Widget的文章就介绍到这了,更多相关Flutter Widget内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯