为你的Flutter应用添加交互性
如何修改你的应用使其对用户输入做出反应? 在本教程中,你将为一个只包含非交互式widget的应用添加交互性。 具体来说,你将通过创建一个管理两个无状态widget的自定义有状态widget来修改一个图标,使其可点击。
构建布局教程 向你展示了如何为以下截图创建布局。
![布局教程应用](/assets/images/docs/ui/layout/lakes.jpg)
当应用首次启动时,星星是实心的红色,表示这个湖泊之前已被收藏。 星星旁边的数字表示41个人收藏了这个湖泊。完成本教程后, 点击星星将取消其收藏状态, 用轮廓星替换实心星, 并减少计数。再次点击将收藏湖泊, 绘制实心星并增加计数。
![你将创建的自定义widget](/assets/images/docs/ui/favorited-not-favorited.png)
为此,你将创建一个包含星星和计数的单个自定义widget, 它们本身就是widget。点击星星会更改这两个widget的状态,因此同一个widget应该管理两者。
你可以在 步骤2:子类化StatefulWidget中直接接触代码。 如果你想尝试不同的状态管理方法, 请跳到状态管理。
有状态和无状态widget
#一个widget要么是有状态的,要么是无状态的。如果一个widget可以改变——例如,当用户与它交互时——它是 有状态的。
一个_无状态_ widget永远不会改变。 Icon
、IconButton
和Text
是无状态widget的示例。无状态widget是StatelessWidget
的子类。
一个_有状态_ widget是动态的:例如,它可以根据用户交互触发的事件或当它接收数据时改变其外观。 Checkbox
、Radio
、Slider
、 InkWell
、Form
和TextField
是有状态widget的示例。有状态widget是StatefulWidget
的子类。
widget的状态存储在一个State
对象中, 将widget的状态与其外观分开。 状态由可以更改的值组成,例如滑块的当前值或复选框是否被选中。 当widget的状态发生变化时, 状态对象调用setState()
, 告诉框架重绘widget。
创建一个有状态widget
#在本节中,你将创建一个自定义有状态widget。 你将用一个管理一行包含两个子widget的单个自定义有状态widget替换两个无状态widget——实心红星及其旁边的数字计数:一个IconButton
和Text
。
实现自定义有状态widget需要创建两个类:
- 定义widget的
StatefulWidget
的子类。 - 包含该widget的状态并定义widget的
build()
方法的State
的子类。
本节将向你展示如何为lakes应用构建一个名为FavoriteWidget
的有状态widget。 设置完成后,你的第一步是选择如何管理FavoriteWidget
的状态。
步骤0:准备工作
#如果你已经在构建布局教程中构建了应用程序, 请跳到下一节。
- 确保你已经设置了你的环境。
- 创建一个新的Flutter应用。
- 将
lib/main.dart
文件替换为main.dart
。 - 将
pubspec.yaml
文件替换为pubspec.yaml
。 - 在你的项目中创建一个
images
目录,并添加lake.jpg
。
一旦你拥有一个已连接且已启用的设备, 或者你已经启动了iOS模拟器 (Flutter安装的一部分)或Android模拟器(Android Studio安装的一部分),你就可以开始了!
步骤1:决定哪个对象管理widget的状态
#widget的状态可以通过几种方式管理, 但在我们的示例中,widget本身, FavoriteWidget
,将管理它自己的状态。 在这个例子中,切换星星是一个隔离的动作,不会影响父widget或UI的其余部分,因此widget可以在内部处理其状态。
在状态管理中,了解更多关于widget和状态的分离,以及如何管理状态。
步骤2:子类化StatefulWidget
#FavoriteWidget
类管理它自己的状态, 因此它重写createState()
以创建一个State
对象。框架在想要构建widget时调用createState()
。 在这个例子中,createState()
返回一个_FavoriteWidgetState
的实例, 你将在下一步中实现它。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
@override
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
步骤3:子类化State
#_FavoriteWidgetState
类存储在widget生命周期中可能发生更改的可变数据。 当应用程序首次启动时,UI显示一个实心的红色星星,表示该湖泊具有“收藏”状态,以及41个赞。这些值存储在_isFavorited
和_favoriteCount
字段中:
class _FavoriteWidgetState extends State<FavoriteWidget> {
bool _isFavorited = true;
int _favoriteCount = 41;
该类还定义了一个build()
方法, 该方法创建一个包含红色IconButton
和Text
的行。你使用IconButton
(而不是Icon
), 因为它有一个onPressed
属性,该属性定义了用于处理点击的回调函数(_toggleFavorite
)。 你将在下一步中定义回调函数。
class _FavoriteWidgetState extends State<FavoriteWidget> {
// ···
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(0),
child: IconButton(
padding: const EdgeInsets.all(0),
alignment: Alignment.center,
icon: (_isFavorited
? const Icon(Icons.star)
: const Icon(Icons.star_border)),
color: Colors.red[500],
onPressed: _toggleFavorite,
),
),
SizedBox(
width: 18,
child: SizedBox(
child: Text('$_favoriteCount'),
),
),
],
);
}
// ···
}
_toggleFavorite()
方法在按下IconButton
时被调用,它调用setState()
。 调用setState()
至关重要,因为这会告诉框架widget的状态已更改,并且应该重绘widget。 setState()
的函数参数在以下两种状态之间切换UI:
star
图标和数字41star_border
图标和数字40
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
步骤4:将有状态widget插入widget树
#在应用程序的build()
方法中将自定义有状态widget添加到widget树中。首先,找到创建Icon
和Text
的代码,并将其删除。在同一位置,创建有状态widget:
child: Row(
children: [
// ...
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
const FavoriteWidget(),
],
),
就是这样!当你热重载应用程序时, 星星图标现在应该响应点击了。
问题?
#如果你无法运行你的代码,请在你的IDE中查找可能的错误。调试Flutter应用程序可能会有所帮助。 如果你仍然找不到问题, 请将你的代码与GitHub上的交互式lakes示例进行比较。
如果你仍然有疑问,请参考任何一个开发者社区频道。
本页的其余部分介绍了管理widget状态的几种方法,并列出了其他可用的交互式widget。
状态管理
#谁管理有状态widget的状态?widget本身?父widget?两者?另一个对象? 答案是……这取决于。有几种有效的方法可以使你的widget具有交互性。你,作为widget的设计者, 根据你期望widget的使用方式做出决定。 以下是管理状态最常见的方法:
你如何决定使用哪种方法? 以下原则应该可以帮助你做出决定:
- 如果相关状态是用户数据, 例如选中或未选中
如果相关状态是用户数据,例如复选框的选中模式或滑块的位置,那么最好由父widget管理状态。
- 如果相关状态是美观的,例如动画,那么最好由widget本身管理状态。
如有疑问,请从在父widget中管理状态开始。
我们将通过创建三个简单的示例:TapboxA、TapboxB和TapboxC来举例说明管理状态的不同方法。这些示例的工作方式都类似——每个示例都创建一个容器,当点击时,容器会在绿色和灰色方块之间切换。_active
布尔值决定颜色:绿色表示活动状态,灰色表示非活动状态。
![活动状态](/assets/images/docs/ui/tapbox-active-state.png)
![非活动状态](/assets/images/docs/ui/tapbox-inactive-state.png)
这些示例使用GestureDetector
来捕获Container
上的活动。
widget自身管理其状态
#有时,让widget在内部管理其状态是最有意义的。例如,ListView
当其内容超过渲染框时会自动滚动。大多数使用ListView
的开发者都不希望管理ListView
的滚动行为,因此ListView
本身管理其滚动偏移量。
_TapboxAState
类:
- 管理
TapboxA
的状态。 - 定义
_active
布尔值,该值决定方块的当前颜色。 - 定义
_handleTap()
函数,该函数在点击方块时更新_active
,并调用setState()
函数来更新UI。 - 实现widget的所有交互行为。
import 'package:flutter/material.dart';
// TapboxA manages its own state.
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
const TapboxA({super.key});
@override
State<TapboxA> createState() => _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
child: Center(
child: Text(
_active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32, color: Colors.white),
),
),
),
);
}
}
//------------------------- MyApp ----------------------------------
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: const Center(
child: TapboxA(),
),
),
);
}
}
父widget管理widget的状态
#通常,让父widget管理状态并在需要更新时告诉子widget是最有意义的。 例如,IconButton
允许你将图标视为可点击的按钮。IconButton
是一个无状态widget,因为我们决定父widget需要知道按钮是否被点击,以便它可以采取适当的行动。
在下面的例子中,TapboxB通过回调将其状态导出到其父级。由于TapboxB不管理任何状态,因此它是StatelessWidget的子类。
ParentWidgetState类:
- 管理TapboxB的
_active
状态。 - 实现
_handleTapboxChanged()
,这是点击方块时调用的方法。 - 当状态发生变化时,调用
setState()
来更新UI。
TapboxB类:
- 扩展StatelessWidget,因为所有状态都由其父级处理。
- 当检测到点击时,它会通知父级。
import 'package:flutter/material.dart';
// ParentWidget manages the state for TapboxB.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
child: TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
const TapboxB({
super.key,
this.active = false,
required this.onChanged,
});
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _handleTap,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
child: Center(
child: Text(
active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32, color: Colors.white),
),
),
),
);
}
}
混合搭配的方法
#对于某些widget,混合搭配的方法是最有意义的。在这种情况下,有状态widget管理部分状态,而父widget管理状态的其他方面。
在TapboxC
示例中,按下时,方块周围会出现一个深绿色的边框。抬起时,边框消失,方块的颜色发生变化。TapboxC
将其_active
状态导出到其父级,但内部管理其_highlight
状态。此示例有两个State
对象,_ParentWidgetState
和_TapboxCState
。
_ParentWidgetState
对象:
- 管理
_active
状态。 - 实现
_handleTapboxChanged()
,这是点击方块时调用的方法。 - 当发生点击并且
_active
状态发生变化时,调用setState()
来更新UI。
_TapboxCState
对象:
- 管理
_highlight
状态。 GestureDetector
监听所有点击事件。当用户按下时,它会添加高亮显示(实现为深绿色边框)。当用户释放点击时,它会删除高亮显示。- 在按下、抬起或取消点击以及
_highlight
状态发生变化时,调用setState()
来更新UI。 - 在点击事件上,使用
widget
属性将状态更改传递给父widget以采取适当的操作。
import 'package:flutter/material.dart';
//---------------------------- ParentWidget ----------------------------
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return SizedBox(
child: TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
const TapboxC({
super.key,
this.active = false,
required this.onChanged,
});
final bool active;
final ValueChanged<bool> onChanged;
@override
State<TapboxC> createState() => _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);
}
@override
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(
width: 200,
height: 200,
decoration: BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? Border.all(
color: Colors.teal[700]!,
width: 10,
)
: null,
),
child: Center(
child: Text(widget.active ? 'Active' : 'Inactive',
style: const TextStyle(fontSize: 32, color: Colors.white)),
),
),
);
}
}
另一种实现方法可能是将高亮状态导出到父级,同时保持活动状态为内部状态,但是如果你让别人使用这个点击框,他们可能会抱怨它没有多大意义。开发者关心的是方块是否处于活动状态。开发者可能并不关心高亮是如何管理的,并且更喜欢点击框处理这些细节。
其他交互式widget
#Flutter 提供了各种按钮和类似的交互式widget。 这些widget中的大多数都实现了Material Design 指南, 该指南定义了一组具有特定UI的组件。
如果你愿意,可以使用GestureDetector
来为任何自定义widget构建交互性。 你可以在状态管理中找到GestureDetector
的示例。在Flutter cookbook中的处理点击食谱中,了解更多关于GestureDetector
的信息。
当你需要交互性时,使用预制widget最简单。这是一个部分列表:
标准widget
#Material组件
#Checkbox
DropdownButton
TextButton
FloatingActionButton
IconButton
Radio
ElevatedButton
Slider
Switch
TextField
资源
#在为你的应用添加交互性时,以下资源可能会有所帮助。
手势,Flutter cookbook中的一个部分。
- 处理手势
- 如何创建一个按钮并使其响应输入。
- Flutter中的手势
- Flutter手势机制的描述。
- Flutter API 文档
- 所有Flutter库的参考文档。
- 精彩应用 运行应用, 代码库
- 一个具有自定义设计和引人入胜交互的Flutter展示应用。
- Flutter的分层设计 (视频)
- 此视频包含有关状态和无状态widget的信息。由Google工程师Ian Hickson讲解。
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。