Flutter进阶—构建布局实例展示了如何创建以下布局。
当应用程序首次启动时,这颗实心星标是红色的,表明这个景点曾经被收藏过。实心星标旁边的人数表明,有66人喜欢这个景点。现在需要完成一个任务,点击该实心星标删除其收藏的状态,用空心星标取代实心星标并减少收藏人数。点击再次收藏景点,画一颗实心星标,增加收藏人数。
要完成此操作,需要创建一个包含Icon(图标)和Text(文本)的自定义控件,Icon(图标)和Text(文本)本身就是控件。因为点击Icon(图标)会改变这两个控件的状态,所以自定义控件应该同时管理两者。
有状态和无状态的控件
有状态和无状态的控件
stateful(无状态)控件没有内部状态来管理。Icon、IconButton和Text是无状态控件的示例,它是StatelessWidget的子类。
stateful(有状态)控件是动态的。用户可以与有状态控件(比如通过键入内容或移动滑块)进行交互,或者随着时间的推移而变化(比如数据Feed会导致UI更新)。Checkbox、Radio、Slider、InkWell、Form和TextField是有状态小部件的示例,它们是StatefulWidget的子类。
创建有状态控件
创建一个自定义的有状态控件,管理具有IconButton和Text两个子控件的行,用来替换两个无状态控件,实心红色星标和数字计数。
实现自定义的有状态控件需要创建两个类
StatefulWidget的子类定义控件。
包含该控件的状态并定义控件的build()方法的State的子类。
第一步:决定哪个对象管理控件的状态
控件的状态可以通过多种方式进行管理,但在下面的示例中,控件本身,FavoriteWidget将管理自己的状态。在这个例子中,切换星标是一个独立的动作,不会影响父控件或用户界面的其余部分,所以该控件可以在内部处理其状态。
第二步:子类StatefulWidget
FavoriteWidget类管理自己的状态,所以它覆盖createState()来创建State对象,框架在构建控件时调用createState()。在这个例子中,createState()创建一个_FavoriteWidgetState的实例。
class FavoriteWidget extends StatefulWidget {
@override
_FavoriteWidgetState createState() => new _FavoriteWidgetState();
}
第三步:子类State
自定义State类存储可变信息(可在控件的整个生命周期内更改的逻辑和内部状态),当应用程序首次启动时,用户界面将显示一个实心的红色星标,表示该景点有“收藏”状态,并有66“收藏”。状态对象将此信息存储在_isFavorited和_favoriteCount变量中。
状态对象还定义了build(),build()创建一行包含红色IconButton和Text。该控件使用IconButton,而不是Icon,因为它具有一个onPressed属性,该属性定义了用于处理点击的回调方法。IconButton也有一个icon 属性保存图标。
_toggleFavorite()方法,当按下IconButton时调用,调用setState()。调用setState()是至关重要的,因为它告诉框架控件的状态已经改变,控件应该重新绘制。
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 66;
void _toggleFavorite() {
setState(() {
// 如果景点目前被收藏
if(_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
// 景点未被收藏
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
@override
Widget build(BuildContext context) {
return new Row(
mainAxisSize: MainAxisSize.min,
children: [
new Container(
padding: new EdgeInsets.all(0.0),
child: new IconButton(
icon: (_isFavorited
? new Icon(Icons.star)
: new Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
)
),
new SizedBox(
width: 18.0,
child: new Container(
child: new Text('$_favoriteCount'),
)
)
]
);
}
}
第四步:将有状态控件插入控件树
在应用程序的构建方法中将您的自定义有状态控件添加到用户界面。首先找到创建图标和文本的代码,然后将其删除:
// ...
new Icon(
Icons.star,
color: Colors.red[500],
),
new Text('66')
// ...
在同一位置,创建有状态控件:
class _MyHomePageState extends State<MyHomePage> {
// ...
@override
Widget build(BuildContext context) {
Widget titleSection = new Container(
// ...
child: new Row(children: [
new Expanded(
child: new Column(
// ...
),
new FavoriteWidget(),
]
)
);
// ...
}
管理状态
通过以下原则可以帮助您决定如何管理状态
如果要管理的状态是用户数据,例如复选框的勾选、未选中的模式,或者滑块的位置,则状态最好由父控件管理。
如果要管理的状态是美观效果,例如动画,则状态最好由控件本身管理。
我们将通过创建三个简单的示例来演示管理状态的不同方式:TapboxA、TapboxB和TapboxC。这些例子都是类似的,每个都创建一个容器,当点击时,在一个绿色或灰色框之间切换。布尔值_active确定颜色,绿色为活动、灰色为非活动。
控件管理自己的状态
有时,这个控件最有意义的是在内部管理它的状态。例如,当ListView的内容超过渲染框时,ListView会自动滚动。大多数使用ListView的开发人员不想管理ListView的滚动行为,因此ListView本身管理其滚动偏移。
_TapboxAState类:
管理TapboxA的状态。
定义确定容器当前颜色的布尔值_active。
定义_handleTap()函数,当该容器被点击时,该函数会更新_active,并调用setState()函数来更新用户界面。
实现控件的所有交互式行为。
//------------------------- TapboxA ---------------------------------
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 ? '有效的' : '无效的',
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],
)
)
);
}
}
//------------------------- MyApp ----------------------------------
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo')
),
body: new Center(
child: new TapboxA(),
)
)
);
}
}
父控件管理控件的状态
通常情况下,父控件最大的意义的是管理状态,并在更新时告诉其子控件。例如,IconButton允许您将图标作为可点击按钮。IconButton是一个无状态的控件,因为我们决定父控件需要知道按钮是否被点击,所以可以采取适当的措施。
在以下示例中,TapboxB通过回调将其状态导出到其父级。因为TapboxB不管理任何状态,它会对StatelessWidget进行子类化。
ParentWidgetState类:
管理TapboxB的_active状态。
实现_handleTapboxChanged(),当容器被点击时调用的方法。
当状态改变时,调用setState()来更新用户界面。
TapboxB类:
扩展StatelessWidget,因为所有状态都由其父进程处理。
当检测到点击时,它会通知父级。
//------------------------ ParentWidget --------------------------------
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,
)
);
}
}
//------------------------- TapboxB ---------------------------------
class TapboxB extends StatelessWidget {
TapboxB({
Key key,
this.active: false,
// import 'package:flutter/foundation.dart';
@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 ? '有效的' : '无效的',
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],
)
)
);
}
}
混合管理状态的方法
对于一些控件,混合管理状态是很有必要的。在这种情况下,状态控件管理某些状态,父控件管理状态的其他方面。
在TapboxC示例中,点击按钮,点击框的周围会出现一个深绿色的边框。不点按钮,边框消失。 TapboxC将其_active状态导出到其父级,但在内部管理其_highlight状态。此示例有两个状态对象 _ParentWidgetState和_TapboxCState。
_ParentWidgetState对象
管理_active状态。
实现_handleTapboxChanged(),当点击框被点击时调用的方法。
调用setState()以在点击发生时更新用户界面,并且_active状态更改。
_TapboxCState对象
管理_highlight状态。
GestureDetector侦听所有点击事件。随着用户点击,它增加了高亮,即深绿色边框。当用户释放点击时,它会删除高亮。
调用setState()通过轻按、点按或点击取消来更新用户界面,而且_highlight状态更改。
在点击事件上,将状态更改传递给父控件,以使用widget属性采取适当的操作。
//---------------------------- ParentWidget ----------------------------
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 TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
)
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
TapboxC({
Key key,
this.active: false,
// import 'package:flutter/foundation.dart';
@required this.onChanged
}) :super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
_TapboxCState createState() => new _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState((){
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState((){
_highlight = false;
});
}
void _handleTapCancel() {
setState((){
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
Widget build(BuildContext context) {
return new GestureDetector(
onTapDown: _handleTapDown,
onTapUp: _handleTapUp,
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: new Container(
child: new Center(
child: new Text(
widget.active ? '有效的' : '无效的',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
)
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? new Border.all(
color:Colors.teal[700],
width: 10.0)
:null,
)
)
);
}
}
实际我们也可以将高亮状态导出到父级,同时保持内部活动状态,但是如果您要求别人使用该点击框,别人可能会抱怨说它没有任何意义。开发人员关心该点击框是否处于活动状态,但开发人员可不在乎如何高亮显示,并且更喜欢点击框自己处理这些细节。