Flutter入门三部曲(2) - 界面开发基础
上一节我们熟悉了初始化后的flutter的界面。这一节,我们就来重点了解一下这部分的内容。
StatelessWidgets and StatefulWidgets
- Flutter中的Widget都必须从Flutter库中继承。
你将使用的两个几乎总是StatelessWidget
和StatefulWidget
。顾名思义,我们只要如果是不需要根据状态变化的组件,我们可以直接继承StatelessWidget
.如果和状态有关系的组件就必须继承StatefulWidget
。 - Flutter中的
Widget
都是不可变的状态。
但是实际上,总要根据对应的状态,视图发生变化,所以就有了state
。用它来保持我们的状态。
这样,一个Stateful Widget,实际上是两个类:状态对象state
和Widget
组成的。
如下代码
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
-
setState
&build
_MyHomePageState
继承于State
.一方面需要管理自己的状态_counter
,一方面需要build
来构造组件。
改变状态后,需要通过setState
来重新构建widget
,就是会重新调用build
方法,来得到状态同步。
最常见的Widget
接着先看看一些常用的组件,这些是随时可用的小部件,开箱即用,你会非常满意:
-
Text
- 用于简单地在屏幕上显示文本的小部件。 -
Image
- 用于显示图像。 -
Icon
- 用于显示Flutter的内置Material和Cupertino图标。 -
Container
- 在Flutter中,相当于div
。允许在其中进行添加填充,对齐,背景,力大小以及其他东西的加载。空的时候也会占用0px的空间,这很方便。 - TextInput - 处理用户反馈。
-
Row
,Column
- 这些小部件显示水平或垂直方向的子项列表。 -
Stack
- 堆栈显示一个孩子的列表。这个功能很像CSS中的'position'属性。 -
Scaffold
- 为应用提供基本的布局结构。它可以轻松实现底部导航,appBars,后退按钮等。
更多的可以看目录。
注意:如果您熟悉基于组件的框架(如React或Vue),则可能不需要阅读此内容。Widget
就是组件。
封装组件
这样的话,实际开发中,也是通过不断对组件的封装,来提高工作效率。
比如简单的封装一个原型的图片组件(实际上,应该这个width和height都可以封装进去的。)
class CircleImage extends StatelessWidget {
final String renderUrl;
CircleImage(this.renderUrl);
@override
Widget build(BuildContext context) {
return Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(renderUrl ?? ''),
),
),
);
}
}
//直接使用
new CircleImage('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533638174553&di=6913961a358faf638b6233e5d3dcc2b2&imgtype=0&src=http%3A%2F%2Fimage.9game.cn%2F2015%2F3%2F5%2F10301938.png')
看看加在上一遍文章下面的效果。(中间皮卡丘)
image.png
Stateful Widget 的生命周期
现在让我们深入一点,
先来思考一下
- 为什么Stateful Widget会将State
和Widget
分开呢?
- 答案就只有一个:性能。
-
State
管理着状态,它是常驻的。然而,Widget
是不可变的,当配置发生变化,它会立马发生重建。所以这样的重建的成本是极低的。
因为State
在每次重建时都没有抛弃,所以可以维护它并且不必每次重建某些东西时都要进行昂贵的计算以获得状态属性。 - 此外,这是允许Flutter动画存在的原因。因为
State
没有丢弃,它可以不断重建它的Widget以响应数据变化。
1. createState()
当创建一个StatefulWidget
时。立即调用。通常都是如下,这样简单的操作。
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
2. mounted is true
当这个Widget
调用createState
后, 会将buildContext
传入。 BuildContext
内有自己在widget tree
上相关的信息。
所有的widgets
都有 bool this.mounted
这个属性. 当BuildContext
传入时,它将会被标记成 true。如果这个属性不是true的话,调用setState
会报错。
注意:你可以在调用setState
前,检查一下这个变量。
if (mounted) {...` to make sure the State exists before calling `setState()
3 . initState()
这个方法只会调用一次,在这个Widget
被创建之后。它必须调用super.initState()
.
在这里可以做:
- 初始化根据对应
BuildContext
的状态 - 初始化根据在树上的父节点的属性确定的值
- 注册
Streams
ChangeNotifiers
或者其他会改变的数据的监听。
@override
initState() {
super.initState();
// Add listeners to this class
cartItemStream.listen((data) {
_updateWidget(data);
});
}
4. didChangeDependencies()
它是在initState
方法后,就会调用。
当Widget
依赖的一些数据(比如说是InheritedWidget
,后面会介绍)更新时,它会立即被调用。
同时build
方法,会自动调用。
需要注意的是,你需要通过调用BuildContext.inheritFromWidgetOfExactType
,手动去注册InheritedWidget
的监听后,这个方法才会起作用。
文档还建议,当InheritedWidget更新时,如果需要进行网络调用(或任何其他昂贵的操作),它可能会很有用。
5.build()
这个方法会经常被调用。
6. didUpdateWidget(Widget oldWidget)
如果父组件发生变化,而且必须去重建widget时,而且被相同的runtimeType
重建时,这个方法会被调用。
因为Flutter是复用state
的。所以,你可能需要重新初始化状态。
如果你的Widget
是需要根据监听的数据,发生变化的,那么你就需要从旧的对象中反注册,然后注册新的对象。
注意:如果您希望重建与此状态关联的Widget,则此方法基本上是'initState'的替代!
这个方法,会自动调用build
,所以不需要去调用setState
@override
void didUpdateWidget(Widget oldWidget) {
if (oldWidget.importantProperty != widget.importantProperty) {
_init();
}
}
7. setState()
这个方法会被framework
和开发者不断调用。用来通知组件刷新。
这个方法的不能有异步的回调。其他,就可以随便使用。
void updateProfile(String name) {
setState(() => this.name = name);
}
8. deactivate()
(这个状态暂时不是很理解)
State
从树中删除时会调用Deactivate
,但可能会在当前帧更改完成之前重新插入。此方法的存在主要是因为State
对象可以从树中的一个点移动到另一个点。
这很少使用。
9. dispose()
State删除对象时调用Dispose ,这是永久性的。
在此方法取消订阅并取消所有动画,流等
10. mounted is false
state
对象被移除了,如果调用setState
,会抛出的错误。
一些疑问
BuildContext
- 1. 每个widget
都有自己的context
。这个context是父组件通过build方法给他返回的。
首先,先看下面代码。我们将在四个地方打印context的hashCode,来看看有什么不同
//...
_MyHomePageState() {
//1. constructor
print('constructor context hashcode = ${context.hashCode}');
}
void _incrementCounter() {
//2. member method
print('_incrementCounter context hashcode = ${context.hashCode}');
setState(() {
_counter++;
});
}
@override
void initState() {
super.initState();
//3. initState
print('initState context hashcode = ${context.hashCode}');
}
@override
Widget build(BuildContext context) {
return new Scaffold(
//...
floatingActionButton: new FloatingActionButton(
onPressed: () {
//4.floattingbutton
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
_incrementCounter();
},
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
image.png
很明显可以看到,我们在initState
方法时,已经分配拿到了父组件的BuildContext.接下来的直接使用context,也都是同一个。
我们知道可以通过Scaffold的context来弹出一个SnackBar
。这里想通过点击弹出这个。
修改代码如下:
//...
floatingActionButton: new FloatingActionButton(
onPressed: () {
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('I am context from Scaffold'),
));
},
tooltip: 'Increment',
child: new Icon(Icons.add),
),
运行,但是运行报错信息如下:
image.png
很明显。通过上面的测试,我们知道这里的context,确实不是Scaffold。那我们要如何在这里拿到Scaffold的context呢?
2. 通过builder方法
修改代码如下,通过Builder方法,得到这个context
.
//...
floatingActionButton: new Builder(
builder: (context) {
return new FloatingActionButton(
onPressed: () {
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('I am context from Scaffold'),
));
_incrementCounter();
},
tooltip: 'Increment',
child: new Icon(Icons.add),
);
},
)
运行结果
image.png
我们可以看到,我们确实拿到了Scaffold
分配的Context
,而且弹出了SnackBar
.
后续过程中,一定要注意这个Context的使用。
注意:这里其实还有另外一个方法,来得到这个BuildContext
。就是将FloatingActionButton分离出来,写成另外一个组件,就能通过build
方法拿到了。
方法如下:
- 添加类
class ScaffoldButton extends StatelessWidget {
ScaffoldButton({this.onPressedButton});
final VoidCallback onPressedButton;
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('I am context from Scaffold')));
onPressedButton();
},
tooltip: 'Increment',
child: new Icon(Icons.add),
);
}
}
再将floatingActionButton
修改成这个类
//...
floatingActionButton: ScaffoldButton(
onPressedButton: () {
_incrementCounter();
},
));
不知所云的构造参数 Key
随意点开一个Widget
,就会发现,可以传递一个参数Key
.那这个Key到底是干啥子,有什么用呢?
Flutter是受React启发的,所以Virtual Dom的diff算法也参考过来了(应该是略有修改),在diff的过程中如果节点有Key来比较的话,能够最大程度重用已有的节点(特别在列表的场景),除了这一点这个Key也用在很多其他的地方这个以后会总结一下。总之,这里我们可以知道key能够提高性能,所以每个Widget都会构建方法都会有一个key的参数可选,贯穿着整个框架。
通常情况下,我们不需要去传递这个Key
。因为framework
会在内部自处理它,来区分不同的widgets
下面有几种情况,我们可以使用它
- 使用ObjectKey
和ValueKey
来对组件进行区分。
可以看PageStorageKey, 和另外一个例子,这个例子是deletion: https://flutter.io/cookbook/gestures/dismissible/.
简单的来说,当我们使用Row
或者Column
时,想要执行一个remove
的动画
new AnimatedList(
children: [
new Card(child: new Text("foo")),
new Card(child: new Text("bar")),
new Card(child: new Text("42")),
]
)
当我们移除"bar"后
new AnimatedList(
children: [
new Card(child: new Text("foo")),
new Card(child: new Text("42")),
]
)
因为我们没有定义Key,所以可能flutter并不知道,我们那个item发生了改变,所以可能发生在位置1上的动画,可能发生在其他位置。
正确的修改如下:
new AnimatedList(
children: [
new Card(key: new ObjectKey("foo"), child: new Text("foo")),
new Card(key: new ObjectKey("bar"), child: new Text("bar")),
new Card(key: new ObjectKey("42"), child: new Text("42")),
]
)
这样当我们移除"bar"的时候,flutter就能准确的区别到正确的位置上。
Key
虽然不是Index
,但是对于每一个元素来说,是独一无二的。
- 使用GlobalKey
- 使用
GlobalKey
的场景是,从父控件和跨子Widget
来传递状态时。
需要注意的是:不要滥用GlobalKey,如果有更好的方式的,请使用其他方式来传递状态。
这里有一个例子是 通过给Scaffold添加GolbalKey。然后通widget.GolbalKey.state来调用showSnackBar
class _MyHomePageState extends State<MyHomePage> {
final globalKey =
new GlobalKey<ScaffoldState>();
void _incrementCounter() {
globalKey.currentState
.showSnackBar(SnackBar(content: Text('I am context from Scaffold')));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
key: globalKey,
//...
)
}
}
这样就可以直接从父控件调用子Widget
的状态。
- 还有一个场景是,过渡动画,当两个页面都是相同的Widget时,也可以使用GlobalKey。
总结
这边文章,我们对StateFulWidget有了升入的认识。
- 认识了通用的控件
- 了解了StatefulWidget的生命周期
- 对BuildContext 了解。
- 对Key的场景进行了了解。得到了使用GlobalKey来跨子组件传递状态的方式。
下一遍文章:我们将更加深入的对Flutter的界面开发的一些原理
参考文章
Flutter Widgets
Flutter中的Key,LocalKey,GlobalKey... And More
what-are-keys-used-for-in-flutter-framework