为你的 Flutter 应用添加交互性
如何修改你的应用以使其对用户输入做出反应?在本教程中,你将为一个只包含非交互式组件的应用添加交互性。具体来说,你将通过创建一个管理两个无状态组件的自定义有状态组件来使图标可点击。
构建布局教程向你展示了如何为以下截图创建布局。

当应用首次启动时,星星是实心的红色,表示这个湖泊之前已经被收藏。星星旁边的数字表示有 41 人收藏了这个湖泊。完成本教程后,点击星星将移除其收藏状态,将实心星星替换为轮廓,并减少计数。再次点击则收藏该湖泊,绘制一个实心星星并增加计数。

为了实现这一点,你将创建一个包含星星和计数的单个自定义组件,它们本身就是组件。点击星星会改变这两个组件的状态,因此同一个组件应该管理两者。
你可以直接在步骤 2:继承 StatefulWidget中触及代码。如果你想尝试不同的状态管理方式,请跳到管理状态。
有状态和无状态组件
#组件要么是有状态的,要么是无状态的。如果组件可以改变——例如,当用户与它交互时——它就是有状态的。
一个_无状态_组件永远不会改变。Icon
、IconButton
和Text
是无状态组件的例子。无状态组件继承StatelessWidget
。
一个_有状态_组件是动态的:例如,它可以响应用户交互触发的事件或接收到数据时改变其外观。Checkbox
、Radio
、Slider
、InkWell
、Form
和TextField
是有状态组件的例子。有状态组件继承StatefulWidget
。
组件的状态存储在State
对象中,将组件的状态与其外观分离。状态由可以改变的值组成,例如滑块的当前值或复选框是否被选中。当组件的状态改变时,状态对象会调用 setState()
,通知框架重新绘制组件。
创建一个有状态组件
#在本节中,你将创建一个自定义的有状态组件。你将用一个自定义的有状态组件替换两个无状态组件——实心红色星星和它旁边的数字计数——该组件管理一个带有两个子组件的行:一个 IconButton
和 Text
。
实现自定义有状态组件需要创建两个类
- 定义组件的
StatefulWidget
子类。 - 包含该组件状态并定义组件
build()
方法的State
子类。
本节将向你展示如何为 lakes 应用构建一个有状态组件,名为 FavoriteWidget
。设置完成后,你的第一步是选择如何管理 FavoriteWidget
的状态。
步骤 0:准备就绪
#如果你已经通过构建布局教程创建了应用,请跳到下一节。
- 确保你已设置好环境。
- 创建一个新的 Flutter 应用.
- 将
lib/main.dart
文件替换为main.dart
。 - 将
pubspec.yaml
文件替换为pubspec.yaml
。 - 在你的项目中创建一个
images
目录,并添加lake.jpg
。
一旦你连接并启用了设备,或者你已经启动了iOS 模拟器(Flutter 安装的一部分)或Android 模拟器(Android Studio 安装的一部分),你就可以开始了!
步骤 1:决定哪个对象管理组件的状态
#组件的状态可以通过多种方式管理,但在我们的例子中,组件本身,即 FavoriteWidget
,将管理自己的状态。在这个例子中,切换星星是一个独立的操作,不影响父组件或用户界面的其他部分,因此组件可以在内部处理其状态。
在管理状态中了解更多关于组件和状态分离以及状态如何管理的信息。
步骤 2:继承 StatefulWidget
#FavoriteWidget
类管理自己的状态,因此它会覆盖 createState()
以创建一个 State
对象。当框架想要构建组件时,会调用 createState()
。在这个例子中,createState()
返回一个 _FavoriteWidgetState
实例,你将在下一步中实现它。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
@override
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
步骤 3:继承 State
#_FavoriteWidgetState
类存储在组件生命周期中可以更改的可变数据。当应用首次启动时,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'))),
],
);
}
// ···
}
当 IconButton
被按下时调用的 _toggleFavorite()
方法会调用 setState()
。调用 setState()
至关重要,因为它告诉框架组件的状态已更改并且组件应该被重新绘制。 setState()
的函数参数在以下两种状态之间切换 UI
- 一个
star
图标和数字 41 - 一个
star_border
图标和数字 40
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
步骤 4:将有状态组件插入组件树
#将你的自定义有状态组件添加到应用 build()
方法中的组件树。首先,找到创建 Icon
和 Text
的代码,并删除它。在相同位置,创建有状态组件
child: Row(
children: [
// ...
Icon(
Icons.star,
color: Colors.red[500],
),
const Text('41'),
const FavoriteWidget(),
],
),
就这样!当你热重载应用时,星形图标现在应该会响应点击。
有问题?
#如果你的代码无法运行,请在 IDE 中查找可能的错误。调试 Flutter 应用可能会有所帮助。如果仍然无法找到问题,请将你的代码与 GitHub 上的交互式 lakes 示例进行对照检查。
如果你仍然有问题,请参阅任何开发者社区频道。
本页的其余部分介绍了组件状态的几种管理方式,并列出了其他可用的交互式组件。
状态管理
#谁管理有状态组件的状态?组件本身?父组件?两者兼有?还是另一个对象?答案是……视情况而定。有几种有效的方法可以使你的组件具有交互性。作为组件设计者,你需要根据你期望组件如何使用来做出决定。以下是管理状态最常见的方式
你如何决定使用哪种方法?以下原则应有助于你做出决定
如果所讨论的状态是用户数据,例如复选框的选中或未选中模式,或者滑块的位置,那么最好由父组件管理状态。
如果所讨论的状态是美学方面的,例如动画,那么最好由组件本身管理状态。
如有疑问,请从在父组件中管理状态开始。
我们将通过创建三个简单示例来展示管理状态的不同方式:TapboxA、TapboxB 和 TapboxC。这些示例都以类似的方式工作——每个示例都创建一个容器,当点击时,在绿色或灰色框之间切换。 _active
布尔值决定颜色:绿色表示活动,灰色表示非活动。


这些示例使用 GestureDetector
来捕获 Container
上的活动。
组件管理自己的状态
#有时,让组件在内部管理其状态是最有意义的。例如,ListView
在其内容超出渲染框时会自动滚动。大多数使用 ListView
的开发人员不希望管理 ListView
的滚动行为,因此 ListView
本身管理其滚动偏移。
_TapboxAState
类
- 管理
TapboxA
的状态。 - 定义
_active
布尔值,它决定了盒子当前的颜色。 - 定义
_handleTap()
函数,该函数在盒子被点击时更新_active
并调用setState()
函数来更新 UI。 - 实现组件的所有交互行为。
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()),
),
);
}
}
父组件管理组件的状态
#通常情况下,由父组件管理状态并告知其子组件何时更新是最合理的。例如,IconButton
允许你将图标视为可点击的按钮。 IconButton
是一个无状态组件,因为我们决定父组件需要知道按钮是否已被点击,以便它可以采取适当的操作。
在下面的示例中,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),
),
),
),
);
}
}
混合搭配方法
#对于某些组件,混合搭配的方法最有意义。在这种情况下,有状态组件管理部分状态,而父组件管理状态的其他方面。
在 TapboxC
示例中,点击时,一个深绿色边框会出现在盒子周围。抬起点击时,边框消失,盒子颜色改变。 TapboxC
将其 _active
状态导出到其父级,但内部管理其 _highlight
状态。此示例有两个 State
对象, _ParentWidgetState
和 _TapboxCState
。
_ParentWidgetState
对象
- 管理
_active
状态。 - 实现
_handleTapboxChanged()
,这是当盒子被点击时调用的方法。 - 当发生点击且
_active
状态改变时,调用setState()
以更新 UI。
_TapboxCState
对象
- 管理
_highlight
状态。 GestureDetector
监听所有点击事件。当用户按下时,它添加高亮(实现为深绿色边框)。当用户释放点击时,它移除高亮。- 在按下、抬起或取消点击时,以及
_highlight
状态改变时,调用setState()
来更新 UI。 - 在点击事件上,使用
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),
),
),
),
);
}
}
另一种实现方式可能是将高亮状态导出到父级,同时将活动状态保留在内部,但如果你让某人使用那个点击框,他们可能会抱怨这没有太大意义。开发人员关心的是盒子是否处于活动状态。开发人员可能不关心高亮如何管理,并且更喜欢点击框处理这些细节。
其他交互式组件
#Flutter 提供了各种按钮和类似的交互式组件。这些组件大多数都实现了Material Design 指南,该指南定义了一组具有特定 UI 风格的组件。
如果你愿意,可以使用GestureDetector
将交互性构建到任何自定义组件中。你可以在管理状态中找到 GestureDetector
的示例。在 Flutter Cookbook 中的处理点击食谱中了解更多关于 GestureDetector
的信息。
当你需要交互性时,使用预制组件是最简单的。以下是部分列表
标准组件
#Material Components
#资源
#以下资源可能有助于为你的应用添加交互性。
手势,Flutter Cookbook 中的一个章节。
- 处理手势
- 如何创建按钮并使其响应输入。
- Flutter 中的手势
- Flutter 手势机制的描述。
- Flutter API 文档
- 所有 Flutter 库的参考文档。
- Wonderous 应用正在运行的应用,仓库
- 一个具有自定义设计和引人入胜交互的 Flutter 展示应用。
- Flutter 的分层设计(视频)
- 此视频包含有关有状态和无状态组件的信息。由 Google 工程师 Ian Hickson 主讲。