Flutter入门:Flutter 的基础Widget

Posted by alonealice on 2020-12-30

Flutter中几乎所有的对象都是一个Widget。它不仅可以表示UI元素,也可以表示一些功能性的组件。Flutter中真正代表屏幕上显示元素的类是Element,也就是说Widget只是描述Element的配置数据。Widget只是UI元素的一个配置数据,并且一个Widget可以对应多个Element

StatelessWidget

StatelessWidget用于不需要维护状态的场景,它通常在build方法中通过嵌套其它Widget来构建UI,在构建过程中会递归的构建其嵌套的Widget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 build(BuildContext context) {
return Echo(text: "hello world");
}
Context

context是当前widget在widget树中位置中执行”相关操作“的一个句柄,比如它提供了从当前widget开始向上遍历widget树以及按照widget类型查找父级widget的方法

1
2
3
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title;

StatefulWidget

StatefulWidget也是继承自Widget类,并重写了createElement()方法,不同的是返回的Element 对象并不相同;另外StatefulWidget类中添加了一个新的接口createState()

一个StatefulWidget类会对应一个State类,State表示与其对应的StatefulWidget要维护的状态,State中的保存的状态信息可以:

  1. 在widget 构建时可以被同步读取。
  2. 在widget生命周期中可以被改变,当State被改变时,可以手动调用其setState()方法通知Flutter framework状态发生改变,Flutter framework在收到消息后,会重新调用其build方法重新构建widget树,从而达到更新UI的目的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});

final int initValue;

@override
_CounterWidgetState createState() => new _CounterWidgetState();
}


class _CounterWidgetState extends State<CounterWidget> {
int _counter;

@override
void initState() {
super.initState();
//初始化状态
_counter=widget.initValue;
print("initState");
}

@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//点击后计数器自增
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}

@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}

@override
void deactivate() {
super.deactivate();
print("deactive");
}

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

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

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

}

Widget build(BuildContext context) {
return CounterWidget();
}

页面启动时:

1
2
3
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

点击按钮热重载时:

1
2
3
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build

在widget树中移除CounterWidget时:

1
2
3
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

各个回调函数:

  • initState:当Widget第一次插入到Widget树时会被调用,对于每一个State对象,Flutter framework只会调用一次该回调,所以,通常在该回调中做一些一次性的操作,如状态初始化、订阅子树的事件通知等。不能在该回调中调用BuildContext.dependOnInheritedWidgetOfExactType,原因是在初始化完成后,Widget树中的InheritFromWidget也可能会发生变化,所以正确的做法应该在在build()方法或didChangeDependencies()`中调用它。
  • didChangeDependencies():当State对象的依赖发生变化时会被调用;例如:在之前build() 中包含了一个InheritedWidget,然后在之后的build()InheritedWidget发生了变化,那么此时InheritedWidget的子widget的didChangeDependencies()回调都会被调用。典型的场景是当系统语言Locale或应用主题改变时,Flutter framework会通知widget调用此回调。
  • 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则会调用此回调。正如之前所述,Widget.canUpdate会在新旧widget的key和runtimeType同时相等时会返回true,也就是说在在新旧widget的key和runtimeType同时相等时didUpdateWidget()就会被调用。
  • deactivate():当State对象从树中被移除时,会调用此回调。在一些场景下,Flutter framework会将State对象重新插到树中,如包含此State对象的子树在树的一个位置移动到另一个位置时(可以通过GlobalKey来实现)。如果移除后没有重新插入到树中则紧接着会调用dispose()方法。
  • dispose():当State对象从树中被永久移除时调用;通常在此回调中释放资源。

获取State对象

context对象有一个findAncestorStateOfType()方法,该方法可以从当前节点沿着widget树向上查找指定类型的StatefulWidget对应的State对象。

1
2
3
4
5
6
7
8
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
//调用ScaffoldState的showSnackBar来弹出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);

一般来说,StatefulWidget的状态是私有的,但是findAncestorStateOfType也是通用的。在Flutter开发中便有了一个默认的约定:如果StatefulWidget的状态是希望暴露出的,应当在StatefulWidget中提供一个of静态方法来获取其State对象,开发者便可直接通过该方法来获取;如果State不希望暴露,则不提供of方法。

1
2
3
4
5
6
ScaffoldState _state=Scaffold.of(context); 
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
通过GlobalKey

Flutter还有一种通用的获取State对象的方法——通过GlobalKey来获取。

  1. 给目标StatefulWidget添加GlobalKey

    1
    2
    3
    4
    5
    6
    7
    //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
    static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
    ...
    Scaffold(
    key: _globalKey , //设置key
    ...
    )
  2. 通过GlobalKey来获取State对象

    1
    _globalKey.currentState.openDrawer()

GlobalKey是Flutter提供的一种在整个APP中引用element的机制。如果一个widget设置了GlobalKey,那么我们便可以通过globalKey.currentWidget获得该widget对象、globalKey.currentElement来获得widget对应的element对象,如果当前widget是StatefulWidget,则可以通过globalKey.currentState来获得该widget对应的state对象。

注意:使用GlobalKey开销较大,如果有其他可选方案,应尽量避免使用它。另外同一个GlobalKey在整个widget树中必须是唯一的,不能重复。

基础 widgets

Text

Text widget 可以用来在应用内创建带样式的文本。

1
2
3
body: Center(
child: Text("Flutter"),
),

Row

在水平方向创建灵活的布局。

Column

在垂直方向创建灵活的布局。

Stack

Stack widget 不是线性(水平或垂直)定位的,而是按照绘制顺序将 widget 堆叠在一起。你可以用 Positioned widget 作为Stack 的子 widget,以相对于 Stack 的上,右,下,左来定位它们。 Stack 是基于 Web 中的绝对位置布局模型设计的。

1
2
3
4
5
6
body: Stack(
children:[
Positioned(child: Text("Flutter1"),left: 20),
Text("Flutter2")
],
),

Container

Containerwidget 可以用来创建一个可见的矩形元素。 Container 可以使用 BoxDecoration来进行装饰,如背景,边框,或阴影等。Container` 还可以设置外边距、内边距和尺寸的约束条件等。

1
2
3
4
5
6
body: Container(
padding: EdgeInsets.symmetric(horizontal: 80.0),
margin: EdgeInsets.symmetric(vertical: 80.0),
decoration: BoxDecoration(color: Colors.blue[500]),//背景颜色
child: Text("Flutter2"),
),

Material组件

Flutter提供了一套丰富的Material组件,如:ScaffoldAppBarFlatButton等。要使用Material 组件,需要先引入它:

1
import 'package:flutter/material.dart';

Cupertino组件(iOS)

Flutter也提供了一套丰富的Cupertino风格的组件。

1
2
//导入cupertino widget库
import 'package:flutter/cupertino.dart';

状态管理

管理状态的最常见的方法:

  • Widget管理自己的状态。
  • Widget管理子Widget状态。
  • 混合管理(父Widget和子Widget都管理状态)。

如何决定使用哪种管理方法?下面是官方给出的一些原则可以帮助你做决定:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父Widget管理。
  • 如果状态是有关界面外观效果的,例如颜色、动画,那么状态最好由Widget本身来管理。
  • 如果某一个状态是不同Widget共享的则最好由它们共同的父Widget管理。

Widget管理自身状态

_TapboxAState 类:

  • 管理TapboxA的状态。
  • 定义_active:确定盒子的当前颜色的布尔值。
  • 定义_handleTap()函数,该函数在点击该盒子时更新_active,并调用setState()更新UI。
  • 实现widget的所有交互式行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);

@override
_TapboxAState createState() => new _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
bool _active = false;

void _handleTap() {
setState(() {
_active = !_active;
});
}

Widget build(BuildContext context) {
return new GestureDetector(
onTap: _handleTap,
child: new Container(
child: new Center(
child: new Text(
_active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}

父Widget管理子Widget的状态

对于父Widget来说,管理状态并告诉其子Widget何时更新通常是比较好的方式。

ParentWidgetState 类:

  • 为TapboxB 管理_active状态。
  • 实现_handleTapboxChanged(),当盒子被点击时调用的方法。
  • 当状态改变时,调用setState()更新UI。

TapboxB 类:

  • 继承StatelessWidget类,因为所有状态都由其父组件处理。
  • 当检测到点击时,它会通知父组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => new _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;

void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}

@override
Widget build(BuildContext context) {
return new Container(
child: new TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key);

final bool active;
final ValueChanged<bool> onChanged;

void _handleTap() {
onChanged(!active);
}

Widget build(BuildContext context) {
return new GestureDetector(
onTap: _handleTap,
child: new Container(
child: new Center(
child: new Text(
active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}

混合状态管理

_ParentWidgetStateC类:

  • 管理_active 状态。
  • 实现 _handleTapboxChanged() ,当盒子被点击时调用。
  • 当点击盒子并且_active状态改变时调用setState()更新UI。

_TapboxCState 对象:

  • 管理_highlight 状态。
  • GestureDetector监听所有tap事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。
  • 当按下、抬起、或者取消点击时更新_highlight状态,调用setState()更新UI。
  • 当点击时,将状态的改变传递给父组件。

全局状态管理

当应用中需要一些跨组件(包括跨路由)的状态需要同步时,上面介绍的方法便很难胜任了。比如,我们有一个设置页,里面可以设置应用的语言,我们为了让设置实时生效,我们期望在语言状态发生改变时,APP中依赖应用语言的组件能够重新build一下,但这些依赖应用语言的组件和设置页并不在一起,所以这种情况用上面的方法很难管理。这时,正确的做法是通过一个全局状态管理器来处理这种相距较远的组件之间的通信。目前主要有两种办法:

  1. 实现一个全局的事件总线,将语言状态改变对应为一个事件,然后在APP中依赖应用语言的组件的initState 方法中订阅语言改变的事件。当用户在设置页切换语言后,我们发布语言改变事件,而订阅了此事件的组件就会收到通知,收到通知后调用setState(...)方法重新build一下自身即可。
  2. 使用一些专门用于状态管理的包,如Provider、Redux,读者可以在pub上查看其详细信息。