首先,我们先附上官网教程中关于flutter框架的介绍,作为补充。
Flutter官方提供的Flutter框架图
Flutter Framework
这是一个纯 Dart实现的 SDK:
Foundation和Animation、Painting、Gestures 在Google的一些视频中被合并为一个dart UI层,对应的是Flutter中的dart:ui包,它是Flutter引擎暴露的底层UI库,提供动画、手势及绘制能力。
Rendering层是一个抽象的布局层,它依赖于dart UI层,Rendering层会构建一个UI树,当UI树有变化时,会计算出有变化的部分,然后更新UI树,最终将UI树绘制到屏幕上,这个过程类似于React中的虚拟DOM。
Widgets层是Flutter提供的的一套基础组件库,在基础组件库之上,Flutter还提供了 Material 和Cupertino两种视觉风格的组件库。
Flutter Engine
这是一个纯 C++实现的 SDK,其中包括了 Skia引擎、Dart运行时、文字排版引擎等。在代码调用 dart:ui库时,调用最终会走到Engine层,然后实现真正的绘制逻辑。
在我们学习flutter的过程中,很多老师大咖们最喜欢说的一句话就是flutter一切皆widget。
Flutter中Widget不仅可以表示UI元素,也可以表示一些功能性的组件如:用于手势检测的 GestureDetector widget、用于APP主题数据传递的Theme等等,而原生开发中的控件通常只是指UI元素。
我们先来看一下Widget类的声明:
/// Describes the configuration for an [Element].
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
其中有一句注释我特别给大家列出来了:Describes the configuration for an [Element].
在Flutter中,Widget的功能是“描述一个UI元素的配置数据”,也就是说,Widget其实并不是表示最终绘制在设备屏幕上的显示元素,而它只是描述显示元素的一个配置数据。实际上在flutter中真正代表屏幕上显示元素的类是Element。对于我们这些希望能够达到使用目的的初学者来说,我觉得没有必要过于深究。
我们只需要知道:Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element。这是因为同一个Widget对象可以被添加到UI树的不同部分,而真正渲染时,UI树的每一个Element节点都会对应一个Widget对象。
再来看看类接口:
widget类继承自DiagnosticableTree,DiagnosticableTree即“诊断树”,主要作用是提供调试信息。
Key: 这个key属性类似于React/Vue中的key,主要的作用是决定是否在下一次build时复用旧的widget,决定的条件在canUpdate()方法中。
createElement**()**:Flutter Framework在构建UI树时,会先调用此方法生成对应节点的Element对象。此方法是Flutter Framework隐式调用的,在我们开发过程中基本不会调用到。
debugFillProperties(…) 复写父类的方法,主要是设置诊断树的一些特性。
**canUpdate(…)**是一个静态方法,可以理解为是否用新的Widget对象去更新旧UI树上所对应的Element对象的配置。
Widget 类本身是一个抽象类,其中最核心的就是定义了createElement()接口,在Flutter开发中,我们一般都不用直接继承Widget类来实现一个新组件,相反,我们通常会通过继承StatelessWidget或StatefulWidget来间接继承Widget类来实现。
StatelessWidget和StatefulWidget都是直接继承自Widget类,而这两个类也正是Flutter中非常重要的两个抽象类,它们引入了两种Widget模型:有状态的组件(Stateful widget) 和无状态的组件(Stateless widget)。
无状态的组件(Stateless widget)
StatelessWidget用于不需要维护状态的场景,也就是说它初始化后就不会更改,比如标题等等。一般情况下,如果widget对应的ui不需要进行更新时,那么我们就应该使用StatelessWidget。
它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。
StatelessWidget继承自Widget类,重写了createElement()方法:
@override
StatelessElement createElement() => new StatelessElement(this);
StatelessElement 间接继承自Element类,与StatelessWidget相对应(作为其配置数据)。
接下来我们看一个简单的示例:
import "package:flutter/material.dart";
class QStatelessWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Text("hello flutter");
}
}
在main.dart中使用:
1.导入自定义的widget
2.在需要使用的地方创建对应的实例
import "package:flutter/material.dart";
import 'basic/QStatelessWidget.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text("Flutter 基础"),
),
body: Center(
child:QStatelessWidget(),
),
));
}
}
运行结果:
显然这样简单的使用并不能满足我们的胃口,接下来我们在看看怎么使用widget的构造函数进行传参。
按照惯例,widget的构造函数参数应使用命名参数,命名参数中的必要参数要添加@required标注,这样有利于静态代码分析器进行检查。另外,在继承widget时,第一个参数通常应该是Key,另外,如果Widget需要接收子Widget,那么child或children参数通常应被放在参数列表的最后。同样是按照惯例,Widget的属性应尽可能的被声明为final,防止被意外改变。
import "package:flutter/material.dart";
class QStatelessWidget extends StatelessWidget {
QStatelessWidget({Key key, @required this.text, this.fontColor: Colors.green})
: super(key: key);
final String text;
final Color fontColor;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
child: Text(
text,
style: TextStyle(
color: fontColor,
fontSize: 24,
fontFamily: "Courier",
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed),
),
),
);
}
}
其中构造函数:
QStatelessWidget({Key key, @required this.text, this.fontColor: Colors.green})
: super(key: key);
这里使用的是dart的语法糖,(详细请参考dart的相关文档)等价于下面这种形式:
QStatelessWidget(String text, Color fontColor=Colors.green){
this.text = text;
this.fontColor = fontColor;
}
this.fontColor: Colors.green相当于其他语言中的默认参数值,这点大家想必都知道。
build方法有一个context参数,它是BuildContext类的一个实例,表示当前widget在widget树中的上下文,每一个widget都会对应一个context对象(因为每一个widget都是widget树上的一个节点)。实际上,context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法
具体的代码没有什么看的,这里就不在赘述了。在使用的地方我们使用命名参数传递对应的参数即可。
有状态的组件(Stateful widget)
StatefulWidget又被称为有状态组件,开发者可以根据用户的操作来选择性的更新界面上的组件。
和StatelessWidget一样,StatefulWidget也是继承自Widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()。
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
接下来我们创建一个基于statefulwidget的例子。刚创建出来的文件可能向下面的例子一样简单,我们一步步开始。
一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态
createState() 用于创建和Stateful widget相关的状态,它在Stateful widget的生命周期中可能会被多次调用。例如,当一个Stateful widget同时插入到widget树的多个位置时,Flutter framework就会调用该方法为每一个位置生成一个独立的State实例,其实,本质上就是一个StatefulElement对应一个State实例。
State中的保存的状态信息的用处:
1.在widget 构建时可以被同步读取。
2.在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。
接下来我们来个简单的例子:
import 'package:flutter/material.dart';
class QStatefulWidget extends StatefulWidget {
const QStatefulWidget({Key key, this.current: 0}) : super(key: key);
final int current;
@override
_QStatefulWidgetState createState() => _QStatefulWidgetState();
}
class _QStatefulWidgetState extends State {
int current;
final welcomeArray = const [
"Welcome,first.",
"Welcome,second.",
"Welcome,third.",
"Welcome,fourth."
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FlatButton(
child: Text(welcomeArray[current]),
onPressed: () => setState(this.handlePressed),
),
),
);
}
void handlePressed() {
if (current == welcomeArray.length - 1) {
current = 0;
} else {
++current;
}
}
@override
void initState() {
// TODO: implement initState
super.initState();
current = widget.current;
}
}
代码十分简单,在屏幕中央显示一个扁平按钮,当点击的时候我们会切换其上面显示的文字。
在main.dart中指定其初始显示的文字索引
作为我们日后使用最多的widget,我们有必要了解一下它的生命周期,先贴一张StatefulWidget生命周期图
ok,我们将其中用到的方法重载,添加log语句,直观的看下其各个状态的变化。
@override
void initState() {
// TODO: implement initState
super.initState();
print("initState function run");
current = widget.current;
}
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
print("didChangeDependencies function run");
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
print("dispose function run");
}
@override
void reassemble() {
// TODO: implement reassemble
super.reassemble();
print("reassemble function run");
}
@override
void deactivate() {
// TODO: implement deactivate
super.deactivate();
print("deactivate function run");
}
@override
void didUpdateWidget(QStatefulWidget oldWidget) {
// TODO: implement didUpdateWidget
super.didUpdateWidget(oldWidget);
print("didUpdateWidget function run");
}
点击按钮:
initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。
didChangeDependencies:当State对象的依赖发生变化时会被调用。
build:用于构建Widget子树的,会在如下场景被调用:
1.调用initState()之后
2.调用didUpdateWidget()之后。
3.调用setState()之后。
4.调用didChangeDependencies()之后。
5.State对象从树中一个位置移除后(会调用deactivate)又重新插入到树的其它位置之后。
reassemble:在热重载(hot reload)时会被调用,此回调在Release模式下永远不会被调用。
didUpdateWidget:在widget重新构建时,Flutter framework会调用Widget.canUpdate来检测Widget树中同一位置的新旧节点,然后决定是否需要更新,如果Widget.canUpdate返回true则会调用此回调。
deactivate:当State对象从树中被移除时,会调用此回调。
dispose:当State对象从树中被永久移除时调用。
更多的内容我们暂时不深入探究,我们只要了解了其各个状态即可。
另外,Flutter Widget build方法可能会执行多次,所以在build方法内做的事情应该尽可能的少。
在使用StatefulWidget的过程中将会调用到setState((){}) 的代码尽可能的和要更新的视图封装在一个尽可能小的模块里。