前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。
本文的主要内容:
Widget
Stateless Widget
和Stateful Widget
的不同Flutter
中按照是否自身可直接响应用户交互可以将Widget
分为两类:
Widget
自身具有如onTap
这类的属性,可以通过这类属性直接监听用户的点击等事件,典型的比如FlatButton
等Widget
自身没有入onTap
这类属性,不能直接监听用户的点击等事件,典型的比如Icon
等由于第一类比较简单,本文重点介绍一下第二类,即如何为非交互性(不能直接响应用户交互)的Widget
添加交互性, 具体来说,我们将通过创建一个自定义的Statful Widget
来让Icon
具有交互性。
在上一篇文章中我们介绍了如何构建一个下面这样的UI
页面:
当这个app
第一次运行的时候那个星星是红色的,代表这个屏幕中展示的那个图片被用户点击了喜欢,星星后面的数字47代表一共有47个用户点击了喜欢。本文将实现,点击星星后移除喜欢状态,用空心星星替换实心星星并减少星星后面的计数。 再次点击空心星星代表添加喜欢,会绘制一颗实心的星星并增加星星后的数字。
要实现此功能,您将创建一个包含星星和计数的自定义Widget
, 点击星星会更改两个子Widget
的状态,因此自定义的Widget应该同时管理这两个子Widget
(星星和计数)。
首先我将会介绍一点前备知识,如果你只对最终的代码实现感兴趣,你可以直接跳到第2步:创建StatefulWidget的子类,如果你想尝试其他的管理状态的方法,可以直接跳到管理状态一节。
Stateful Widget
和Stateless Widget
一个Widget
要么是有状态(stateful
)的,要么是无状态(stateless
)的,如果一个Widget
是可改变的,比如当用户与其交互的时候其会产生变化,这个Widget
就是有状态的(stateful
)。
一个无状态(stateless
)的Widget是永远不会发生改变的,Icon
、IconButton
、Text
都是典型的无状态的Widget
,无状态(stateless
)的Widget
都是StatelessWidget
的子类。
一个有状态(stateful
)的Widget
是动态的,比如它可以更改其外观以响应用户交互或接收数据时触发的事件。CheckBox
、Radio
、Slider
、InkWell
、Form
、TextField
都是典型的有状态的Widget
,有状态(stateful
)的Widget
都是StatefulWidget
的子类。
Widget
的状态都是保存在State
对象中的,从外观上分析小部件的状态。 状态由可以更改的值组成,例如滑块(slider
)的当前值、是否选中复选框(CheckBox
)。 当Widget
的状态发生变化时,State
对象调用会setState()
方法来告诉框架重绘该Widget
。
stateful
)Widget
明确几点概念:
Widget
一定实现了2个类:StatefulWidget
、State
State
类包含Widget
的可变状态以及build()
方法Widget
的状态(state
)发生了改变,State
对象将会调用setState()
方法高速Flutter
框架需要重绘当前Widget
本节将创建一个自定义的有状态(Stateful
)的Widget
,我们将用我们自定义的包含一个IconButton
和一个Text
的Widget
来替代原有的红色星星Widget
和计数Widget
。
实现一个自定义的Widget
需要创建2个类:
StatefulWidget
类的子类,用于定义Widget
State
类的子类,包含了State
对象,并且定义build()
方法我们通过简单的几步来构建一个名为FavoriteWidget
的自定义Widget
:
Widget
的状态(State
)Widget
的状态(State
)可以有多种管理方式,在此处由于切换星星的状态(实心还是空心)是一个独立的操作,不会影响父Widget
或UI
的其余部分,所以我们让Widget
自己管理自己的状态(State
)。
关于详细的状态管理的内容,我会在后面的管理状态一节介绍。
StatefulWidget
的子类由于第1步我们已经决定了FavoriteWidget
自己管理自己的状态(State
),所以我们应该重写createState()
方法来创建一个State
对象。Flutter
框架会在构建Widget
的时候调用对应Widget
的createState()
方法。在这个例子中,我们应该在createState()
方法中返回一个我们将在下一步定义的_FavoriteWidgetState
类的实例对象:
class FavoriteWidget extends StatefulWidget {
@override
_FavoriteWidgetState createState() => _FavoriteWidgetState();
}
注意:这里的_开头指的是定义的对应类是私有的。
State
类的子类我们定义一个_FavoriteWidgetState
类来存储会在Widget
不同生命周期变化的数据,当app
第一次运行的时候,UI
界面应该展示红色的实心星星,代表当前已经选择了"喜欢"状态,并且傍边展示的文字为"41",我们本别使用bool _isFavorited
和int _favoriteCount
变量来存储这两个状态:
class _FavoriteWidgetState extends State {
bool _isFavorited = true;
int _favoriteCount = 41;
// ···
}
_FavoriteWidgetState
类同样也定义了一个build()
方法,在该方法中创建一个Row
(行),Row
中包含有一个Iconbutton
和一个Text
,我们使用Iconbutton
而不是Icon
的原因是IconButton
有onPressed
属性,我们可以通过这个onPressed
属性定义处理点击事件的回调函数(_toggleFavorite
),我们将在后面具体定义这个_toggleFavorite
函数:
class _FavoriteWidgetState extends State {
// ···
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: EdgeInsets.all(0),
child: IconButton(
icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18,
child: Container(
child: Text('$_favoriteCount'),
),
),
],
);
}
}
注意:我们这里将Text
作为子Widget
放置在了SizedBox
中,并且设置了SizedBox
的宽度,这样做的作用是固定Text
的宽度,设想一下,当Text
中只显示1位数字的时候Text
的宽度和显示2位数字的宽度一定是不一样的,如果不固定Text
的宽度,当数字变化的时候就会出现Text
宽度发生跳变的情况,导致视觉效果很不好。
当IconButton
被点击的时候将会调用_toggleFavorite()
方法,我们在_toggleFavorite()方法中调用setstate()方法并更新状态,这样Flutter框架就会知道需要重新绘制当前Widget
了,从而达到更新界面的效果:
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
setState()
方法中的代码逻辑很简单,首先判断当前_isFavorited
的状态,然后对_isFavorited
和_isFavorited
的值进行更新。
Stateful Widget
加入到Widget
树中我们应该在app
的build()
方法中将我们自定义的Stateful Widget
加入到Widget
树中,首先找到原先Icon
和Text
的位置,然后删除原来的代码,加入新的我们创建的Stateful Widget
:
Widget titleSection = Container(
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/*1*/
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/*2*/
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
FavoriteWidget(),
],
),
);
然后运行代码(推荐使用热更新),可以看到效果图:
在我们的设计中,到底应该由谁来管理Widget
的状态(State
)?是Widget
本身?是Widget
的父Widget
?还是二者共同管理?还是另一个对象来管理? 事实上有不止一种有效的方法可以使你的Widget
小部件具有交互性, 作为Widget
的设计者,你可以根据预期的Widget
的使用方式做出决策。 以下是几种最常用的管理状态的方法:
你可能会有疑问,你应该如何决定具体使用哪一种状态管理方法?这里提供几个原则供你参考:
CheckBox
是否被选中,或者Slider
(进度条)的当前进度,这种情况下最好让Widget
的父级Widget
去管理其状态Widget
自己来管理自己的状态如果你不太确定自己的场景属于以上哪种,可以直接使用父级Widget
管理的方法,因为这个方法是通用的。
接下来我将通过创建三个简单示例(TapboxA
,TapboxB
和TapboxC
)来举例说明管理状态的不同方法。 这几个示例的工作方式类似: 每个都创建了一个Container
,当点击时,可以在绿色或灰色框之间切换, _active
布尔值确定颜色:true
代表绿色,false
代表灰色。
Widget
自己管理自己本身的State
有时,由Widget
自己管理自己的状态可以产生很强大的功能。例如,ListView
在其内容的总尺寸超出其最大渲染框的尺寸时会自动进行滚动,这个滚动的状态是由ListView
自己管理的,不需要我们开发人员去手动设置它什么时候应该开始滚动、什么时候应该停止滚动。
我们通过一个示例来进行说明,我们创建一个_TapboxAState
类:
TapboxA
的状态_activity
,代表当前Widget
的颜色_handleTap()
方法,当Widget
被点击时在该方法中调用setState()
并更新 _activity
的值从而达到更新UI的目的Widget
的所有交互行为代码如下:
// TapboxA 自己管理自己的状态
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);
@override
_TapboxAState createState() => _TapboxAState();
}
class _TapboxAState extends State {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
//------------------------- MyApp ----------------------------------
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter Demo'),
),
body: Center(
child: TapboxA(),
),
),
);
}
}
运行效果如下图所示:
Widget
的父级Widget
管理其State
父Widget
管理子Widget
状态的最大用处是在合适的时机通知子Widget
进行UI
更新。 例如,IconButton
允许你将Icon
视为可点击的按钮, IconButton
是一个无状态的Widget
,所以我们应该通过父Widget
来确定Iconutton
是否已被点击。
在以下例子中,TapboxB
将其状态回调给父Widget
,因为TapboxB
不管理任何状态,所以它是StatelessWidget
的子类。
在这个示例中我们应该实现2个类:ParentWidgetState
(代表父Widget
)、TapboxB
(代表子Widget
)
ParentWidgetState
的主要功能:
TapboxB
管理_activity
状态_handleTapboxChanged()
方法,该方法会在TapboxB
被点击时调用setState()
来更新UI
TapboxB
的主要功能:
StatelessWidget
类,因为TapboxB
不用管理自己的状态tap
)被触发的时候通知父Widget
代码实现如下:
// ParentWidget为TapboxB管理状态.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
代码的运行效果如下:
对于某些Widget
,使用混合的方法管理其状态很有有意义。 在这种情况下,有状态(stateful
)的Widget
和其父Widget
分别管理其一部分状态(State
)。
在TapboxC
示例中,在点击时,框周围会出现深绿色边框,点击后,边框消失,框的颜色也会改变。 TapboxC
将其 _active
状态导出到其父Widget
,在内部管理只其 _highlight
状态,所以 此示例有两个State
对象,_ParentWidgetState
和_TapboxCState
:
_ParentWidgetState
的功能:
_activity
状态_handleTapboxChanged()
方法,该方法会在方框被点击后调用setState()
并改变_activity
的值以更新UI
_TapboxCState
的功能:
_highlight
状态GestureDetector
监听所有的点击事件,当用户手指点下的时候添加高亮边框,当用户手指抬起的时候取消高亮边框Widget
传递的状态进行相应操作//---------------------------- ParentWidget ----------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
TapboxC({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged onChanged;
_TapboxCState createState() => _TapboxCState();
}
class _TapboxCState extends State {
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) {
// This example adds a green border on tap down.
// On tap up, the square changes to the opposite state.
return GestureDetector(
onTapDown: _handleTapDown, // Handle the tap events in the order that
onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: Container(
child: Center(
child: Text(widget.active ? 'Active' : 'Inactive',
style: TextStyle(fontSize: 32.0, color: Colors.white)),
),
width: 200.0,
height: 200.0,
decoration: BoxDecoration(
color:
widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}
运行效果如下所示:
替代实现可能已将高亮状态导出到父级,同时保持活动状态为内部,但如果您要求某人使用该分接框,他们可能会抱怨它没有多大意义。 开发人员关心该框是否处于活动状态。 开发人员可能并不关心如何管理突出显示,并且更喜欢点按框处理这些细节。
Widget
Flutter
提供了很多按钮和类似的交互式Widget
。 这些Widget
中的大多数都实现了Material Design
准则,该准则定义了一组具有固定用户界面的组件。
如果您愿意,可以使用GestureDetector
在任何自定义的Widget
中构建交互性。 您可以在管理状态一节中找到GestureDetector
的使用示例。
提示:
Flutter
还提供了一些IOS
风格的Widget
,称之为Cupertino
,具体地址:https://api.flutter.dev/flutter/cupertino/cupertino-library.html
当您需要交互性时,最简单的方法是使用Flutter
已经给我提供好的Widget
,下面是一个部分列表:
Widget
Form
,地址:https://api.flutter.dev/flutter/widgets/Form-class.html
FormField
,地址:https://api.flutter.dev/flutter/widgets/FormField-class.html
Checkbox
地址:https://api.flutter.dev/flutter/material/Checkbox-class.html
DropdownButton
,地址:https://api.flutter.dev/flutter/material/DropdownButton-class.html
FlatButton
, 地址:https://api.flutter.dev/flutter/material/FlatButton-class.html
FloatingActionButton
, 地址:https://api.flutter.dev/flutter/material/FloatingActionButton-class.html
IconButton
,地址:https://api.flutter.dev/flutter/material/IconButton-class.html
Radio
,地址:https://api.flutter.dev/flutter/material/Radio-class.html
RaisedButton
,地址:https://api.flutter.dev/flutter/material/RaisedButton-class.html
Slider
,地址:https://api.flutter.dev/flutter/material/Slider-class.html
Switch
,地址:https://api.flutter.dev/flutter/material/Switch-class.html
TextField
,地址:https://api.flutter.dev/flutter/material/TextField-class.html
好了,本文的全部内容到这里就结束了,总结一下,本文我们主要介绍了如何给Flutter
中的Widget
添加交互性,下一篇文章我们将介绍如何油压地给Flutter
项目中添加并引用资源和图片,欢迎大家关注。