Flutter开发 12.Widget-StatelessWidget和StatefulWidget
1. Widget绘制原理
在开始之前有必要先了解一下组件显到屏幕上的流程,flutter会生成三棵组件的树型结构,其中有一颗就是渲染树, 前几节课中讲到的Widget组件例如:Text、Icon、GridView等,全是直接或者间接的继承自StatelessWidget,也就是无状态组件,然后StatelessWIdget继随自Widget类,并且重写了createElement方法。
abstract class StatelessWidget extends Widget {
......
@override
StatelessElement createElement() => StatelessElement(this);
......
}
abstract class StatefulWidget extends Widget {
......
@override
StatefulElement createElement() => StatefulElement(this);
......
}
class StatelessElement extends ComponentElement
class StatefulElement extends ComponentElement
那么这个方法将自身的实例对像做为参数,传入创建了一个ComponentElement实例. 也就是说,当我们实现一个widget组合结构时,也会有一个对应的Element组合结构. 此处引用了官方给的一个例子
Container( // 一个容器 widget
color: Colors.blue, // 设置容器背景色
child: Row( // 可以将子widget沿水平方向排列
children: [
Image.network('https://www.example.com/1.png'), // 显示图片的 widget
const Text('A'),
],
),
);
这段代码就对应了一个Widgets的树型结构
因为每个widget还会创建一个element,因此同样会产生一个Element的树形结构
那么最终的渲染树的节点分别都是如何得到的呢? ??
我们先看一下 Icon和Text组件的 Build方法
Icon
Widget iconWidget = RichText(
overflow: TextOverflow.visible, // Never clip.
textDirection: textDirection, // Since we already fetched it for the assert...
text: TextSpan(
text: String.fromCharCode(icon!.codePoint),
style: TextStyle(
inherit: false,
color: iconColor,
fontSize: iconSize,
fontFamily: icon!.fontFamily,
package: icon!.fontPackage,
),),);
Text
Widget result = RichText(
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null.
locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null
softWrap: softWrap ?? defaultTextStyle.softWrap,
overflow: overflow ?? effectiveTextStyle?.overflow ?? defaultTextStyle.overflow,
textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
maxLines: maxLines ?? defaultTextStyle.maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.of(context),
text: TextSpan(
style: effectiveTextStyle,
text: data,
children: textSpan != null ? <InlineSpan>[textSpan!] : null,),);
通过上面的源码可以看到,这两个组件,都是使用了 RichText来创建的Widget对像。
跳转到RichText 的源码,可以找到有一个createRenderObject方法
class RichText extends MultiChildRenderObjectWidget {
......
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context));
return RenderParagraph(text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
locale: locale ?? Localizations.maybeLocaleOf(context),
);
}
接下来再看一下Container的build方法
class Container extends StatelessWidget {
......
@override
Widget build(BuildContext context) {
Widget? current = child;
if (child == null && (constraints == null || !constraints!.isTight)) {
current = LimitedBox(
maxWidth: 0.0,
maxHeight: 0.0,
child: ConstrainedBox(constraints: const BoxConstraints.expand()),
);
}
if (alignment != null)
current = Align(alignment: alignment!, child: current);
final EdgeInsetsGeometry? effectivePadding = _paddingIncludingDecoration;
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color!, child: current);
if (clipBehavior != Clip.none) {
assert(decoration != null);
current = ClipPath(
clipper: _DecorationClipper(
textDirection: Directionality.maybeOf(context),
decoration: decoration!,
),
clipBehavior: clipBehavior,
child: current,
);
}
if (decoration != null)
current = DecoratedBox(decoration: decoration!, child: current);
if (foregroundDecoration != null) {
current = DecoratedBox(
decoration: foregroundDecoration!,
position: DecorationPosition.foreground,
child: current,
);
}
if (constraints != null)
current = ConstrainedBox(constraints: constraints!, child: current);
if (margin != null)
current = Padding(padding: margin!, child: current);
if (transform != null)
current = Transform(transform: transform!, alignment: transformAlignment, child: current);
return current!;
}
可以看到最终return的这个组件具体类型的可能性比较多,有可能是LimitedBox、Padding、ColoredBox、ClipPath、DecoratedBox、ConstrainedBox、Transform,那么我们同样需要看一下,这些组件中是否也有创建renderObject的方法呢
class LimitedBox extends SingleChildRenderObjectWidget {
......
@override
RenderLimitedBox createRenderObject(BuildContext context) {
return RenderLimitedBox(
maxWidth: maxWidth,
maxHeight: maxHeight,
);
}
}
class Padding extends SingleChildRenderObjectWidget {
......
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.maybeOf(context),
);
}
}
class ColoredBox extends SingleChildRenderObjectWidget {
......
@override
RenderObject createRenderObject(BuildContext context) {
return _RenderColoredBox(color: color);
}
}
class DecoratedBox extends SingleChildRenderObjectWidget {
......
@override
RenderDecoratedBox createRenderObject(BuildContext context) {
return RenderDecoratedBox(
decoration: decoration,
position: position,
configuration: createLocalImageConfiguration(context),
);
}
}
class ClipPath extends SingleChildRenderObjectWidget {
......
@override
RenderClipPath createRenderObject(BuildContext context) {
assert(clipBehavior != Clip.none);
return RenderClipPath(clipper: clipper, clipBehavior: clipBehavior);
}
}
上面列举出来的这些源可以看到, 全部都包含了 createRenderObject ,这个方法就是生成渲染节点,那么最只需要有一个时机来调用创建得到这个对象就可以了。
我们先看一下ComponentElement的源码:
abstract class ComponentElement extends Element {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_lifecycleState == _ElementLifecycle.active);
_firstBuild();
assert(_child != null);
}
}
我们只看一下这个挂载方法(mount) 他里边除了调用了父类的方法外,就只执行了_firstBuild(); 我们根据代码逻辑可以得到调用关系如下:
_firstBuild() -> rebuild() -> performRebuild -> build()
接下来我们看下这个build()方法的实现
class StatelessElement extends ComponentElement {
......
@override
Widget build() => widget.build(this); //将自身做为参数,传递给widget.build方法
}
}
class StatefulElement extends ComponentElement {
......
@override
Widget build() => state.build(this); //将自身做为参数,传递给widget.build方法
}
因为StatefulWidget的对像,是在state中实现的build方法(后面会详细说明), 因此Element在调用上做了区分。
abstract class Element extends DiagnosticableTree implements BuildContext {}
@override
Widget build(BuildContext context) { //这个context就是element
......
}
这样我们就清楚的知道了,我们使用组件中的Build方法是在哪里调用的了。并且我们也清楚的知道这个参数就是Element,因为Element实现BuildContext, 但是我们依然没有找到在哪里调用的createRenderObject.
因为我们已经找到了widget与element的一对一的关系,接下来我们看一下上面提到的Text和Icon的具体实现类RichText,先看一下RichText的继承关系。
class RichText extends MultiChildRenderObjectWidget {}
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
......
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}
可以看到这里边创建的Element对像实际是一个MultiChildRenderObjectElement对像。
class MultiChildRenderObjectElement extends RenderObjectElement
abstract class RenderObjectElement extends Element {
......
@override
void mount(Element? parent, Object? newSlot) {
......
_renderObject = widget.createRenderObject(this);
......
}
}
这里就是我们要找的位置了,RenderObjectElement中的mount方法中调用了widget.createRenderObject,而
ComponentElement中的mount调用的方法是build.因此渲染树与element树的数量并不是一一对应的.
2. StatelessWidget
之前我们用过的很多组件都是继承自StatelessWidget,像Container、Text、 StatelessWidget相对比较简单,它继承自widget类,重写了createElement()方法,StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它 widget 来构建UI,在构建过程中会递归的构建其嵌套的 widget 。
无状态组件用于没有变化的情况,比如一个应用的关于页面:
Text spawnRegularText(String stringText) {
return Text(stringText, //文本内容
//文本方向 如果需要使用rtl需要add flutter_localizations package to your pubspec.yml
textDirection: TextDirection.ltr,
textAlign: TextAlign.center, //对齐方式
overflow: TextOverflow.ellipsis, //超长后截断方式 ...
maxLines: 1, //最大行数
textScaleFactor: 1.0, //放大缩小比例
style: const TextStyle(
// fontFamily: "zzFontFamily", //所用的字体、这里用的是自定义的字体
fontStyle: FontStyle.normal, //文本显示样式
// fontWeight: FontWeight.bold, //粗体
letterSpacing: 1, //字母之间的间距
wordSpacing: 2, //单词之间的间距
fontSize: 24, //字体大小
color: Color.fromARGB(255, 174, 0, 255), // 字体颜色
));
}
class ZzAboutWidget extends StatelessWidget {
const ZzAboutWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(30),
child: const Image(
image: NetworkImage(
"https://img-blog.csdnimg.cn/20190520151631821.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3h1YW53ZW5jaGFv,size_16,color_FFFFFF,t_70",
),
width: 120,
height: 120,
fit: BoxFit.fill),
),
Padding(
padding: const EdgeInsets.fromLTRB(0, 20, 0, 30),
child: spawnRegularText("爪爪课堂 v1.0"),
),
const Text("blog.csdn.net/xuanwenchao",
style: TextStyle(
fontSize: 15, //字体大小
color: Color.fromARGB(255, 37, 58, 252), // 字体颜色
))
],
),
);
}
}
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("关于页面"),
),
body: const ZzAboutWidget())));
}
运行一下效果如下:
3.StatefulWidget
有状态组件也就是说数据会变化,也是使用场景最多的组件,StatefulWidget也是继承自widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
......
@protected
State createState();
}
StatefulWidget本身是不提供build方法的,而是通过createState() 创建和 StatefulWidget 相关的状态类,它在StatefulWidget 的生命周期中可能会被多次调用。例如,当一个 StatefulWidget 同时插入到 widget 树的多个位置时,Flutter 框架就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。当State被改变时,可以手动调用其setState()方法通知Flutter 框架状态发生改变,Flutter 框架在收到消息后,会重新调用其build方法重新构建 widget 树,从而达到更新UI的目的。
接下来我们使用一个实例来演示, 首先新建一个CounterWidget.dart文件:
import 'package:flutter/material.dart';
//CounterWidget
class CounterWidget extends StatefulWidget {
//在这个构函数参数中,使用"{}"包围的参数属于可选命名参数
const CounterWidget({Key? key, this.initCounterValue = 0}) : super(key: key);
final int initCounterValue; //计数器的初始值
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
//CounterWidget对应的state类
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
@override
void initState() {
_counter = widget.initCounterValue; //读取组件类中计数器的初始值
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
width: double.infinity,
child: TextButton(
child: Text(
"$_counter",
style: const TextStyle(fontSize: 28, color: Colors.purple),
),
onPressed: () => setState(() {
_counter++;
})),
);
}
}
就像其它的自定义组件一样,我们也继承自StatefulWidget并且创建对应的state类。
然后在main.dart中去使用这个组件.
import 'CounterWidget.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text("Counter页面"),
),
body: const CounterWidget(
initCounterValue: 0,
))));
}
运行一下就可以看到效果,点击一次数字就会加1
这里有一个比较重要的方法叫 setState需要说明一下,首选看一下我们自己实现的State类的原型定义:
abstract class State<T extends StatefulWidget> with Diagnosticable {
......//省略其它代码
@protected
void setState(VoidCallback fn) {
......//省略其它代码
final Object? result = fn() as dynamic;//动态调用了传入的函数VoidCallback
......//省略其它代码
_element!.markNeedsBuild();
}
}
这里以以看到,他调用了element的markNeedsBuild方法,用来标记需要构建。
void markNeedsBuild() {
assert(_lifecycleState != _ElementLifecycle.defunct);
if (_lifecycleState != _ElementLifecycle.active)
return;
......//省略其它代码
if (dirty)
return;
_dirty = true;
owner!.scheduleBuildFor(this);
}
将当前 element 元素标记为“脏数据”(也就是需要更新的意思),scheduleBuildFor方法会启动onBuildScheduled,并且将element添加到_dirtyElements链表里,以便在下一帧更新信号时更新.
void scheduleBuildFor(Element element) {
......//省略其它代码
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
_dirtyElements.add(element);
element._inDirtyList = true;
......//省略其它代码
}