为您的 Flutter 应用添加交互性
如何修改您的应用以使其对用户输入做出反应?在本教程中,您将为仅包含非交互式小部件的应用添加交互性。具体来说,您将修改一个图标以使其可轻触,方法是创建一个自定义有状态小部件来管理两个无状态小部件。
在构建布局教程中,向您展示了如何创建以下屏幕截图的布局。
当应用首次启动时,星星为实心红色,表示该湖泊以前已被收藏。星星旁边的数字表示有 41 人收藏了该湖泊。完成本教程后,轻触星星即可取消收藏状态,实心星星将变为轮廓,并且计数将减少。再次轻触即可收藏湖泊,绘制实心星星并增加计数。
要实现此目的,您将创建一个包含星星和计数的自定义小部件,它们本身就是小部件。轻触星星会更改两个小部件的状态,因此同一个小部件应管理这两个小部件。
您可以直接在步骤 2:子类化 StatefulWidget中轻触代码。如果您想尝试不同的状态管理方式,请跳至管理状态。
有状态和小组件
小组件要么有状态,要么无状态。如果一个小组件可以更改,例如,当用户与其交互时,则它是有状态的。
无状态小组件永远不会更改。Icon
、IconButton
和 Text
是无状态小组件的示例。无状态小组件的子类是 StatelessWidget
。
有状态小组件是动态的:例如,它可以根据用户交互触发的事件或在接收到数据时更改其外观。Checkbox
、Radio
、Slider
、InkWell
、Form
和 TextField
是有状态小组件的示例。有状态小组件的子类是 StatefulWidget
。
小组件的状态存储在 State
对象中,将小组件的状态与其外观分开。状态由可以更改的值组成,例如滑块的当前值或复选框是否已选中。当小组件的状态更改时,状态对象将调用 setState()
,告诉框架重新绘制小组件。
创建有状态小组件
在本部分中,您将创建一个自定义有状态小组件。您将用一个管理带有两个子小组件的行(IconButton
和 Text
)的自定义有状态小组件替换两个无状态小组件(实心红星和旁边的数字计数)。
实现自定义有状态小组件需要创建两个类
- 定义小组件的
StatefulWidget
子类。 - 包含该小组件的状态并定义小组件的
build()
方法的State
子类。
本部分向您展示如何为湖泊应用构建名为 FavoriteWidget
的有状态小组件。在设置后,您的第一步是选择如何管理 FavoriteWidget
的状态。
步骤 0:准备就绪
如果您已经在 构建布局教程 中构建了应用,请跳到下一部分。
- 确保您已 设置 您的环境。
- 创建一个新的 Flutter 应用.
- 用
main.dart
替换lib/main.dart
文件。 - 用
pubspec.yaml
替换pubspec.yaml
文件。 - 在您的项目中创建一个
images
目录,并添加lake.jpg
。
一旦您拥有已连接和启用的设备,或者您已启动 iOS 模拟器(Flutter 安装的一部分)或 Android 模拟器(Android Studio 安装的一部分),您就可以开始了!
步骤 1:决定哪个对象管理小组件的状态
小组件的状态可以通过多种方式管理,但在我们的示例中,小组件本身 FavoriteWidget
将管理其自身的状态。在此示例中,切换星星是一个独立的动作,不会影响父小组件或 UI 的其余部分,因此小组件可以在内部处理其状态。
在 管理状态 中了解有关小组件和状态分离的更多信息,以及如何管理状态。
步骤 2:子类化 StatefulWidget
FavoriteWidget
类管理其自身的状态,因此它重写 createState()
来创建一个 State
对象。当框架想要构建小部件时,它会调用 createState()
。在此示例中,createState()
返回 _FavoriteWidgetState
的实例,您将在下一步中实现它。
class FavoriteWidget extends StatefulWidget {
const FavoriteWidget({super.key});
@override
State<FavoriteWidget> createState() => _FavoriteWidgetState();
}
步骤 3:子类化状态
_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.centerRight,
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
图标和数字 41star_border
图标和数字 40
void _toggleFavorite() {
setState(() {
if (_isFavorited) {
_favoriteCount -= 1;
_isFavorited = false;
} else {
_favoriteCount += 1;
_isFavorited = true;
}
});
}
步骤 4:将有状态小组件插入小组件树
在应用的 build()
方法中将自定义有状态小组件添加到小组件树。首先,找到创建 Icon
和 Text
的代码,然后将其删除。在相同位置创建有状态小组件
@@ -83,11 +83,7 @@
|
|
83
83
|
],
|
84
84
|
),
|
85
85
|
),
|
86
|
-
|
87
|
- Icons.star,
|
88
|
- color: Colors.red[500],
|
89
|
-
),
|
90
|
- const Text('41'),
|
86
|
+ const FavoriteWidget(),
|
91
87
|
],
|
92
88
|
),
|
93
89
|
);
|
就是这样!当你热重载应用时,星星图标现在应该会响应点击。
问题?
如果你无法运行代码,请在 IDE 中查找可能的错误。 调试 Flutter 应用 可能会有帮助。如果你仍然找不到问题,请在 GitHub 上根据交互式湖泊示例检查你的代码。
如果你仍然有疑问,请参考任何一个开发者 社区 频道。
本页的其余部分介绍了管理小组件状态的几种方法,并列出了其他可用的交互式小组件。
管理状态
谁管理有状态小组件的状态?小组件本身?父小组件?两者?另一个对象?答案是……这取决于。有几种有效的方法可以使你的小组件具有交互性。作为小组件设计者,你可以根据你期望小组件如何使用来做出决定。以下是管理状态的最常见方法
如何决定使用哪种方法?以下原则应帮助你做出决定
-
如果所讨论的状态是用户数据,例如复选框的选中或未选中模式,或滑块的位置,那么最好由父级小组件管理状态。
-
如果所讨论的状态是美观的,例如动画,那么最好由小组件本身管理状态。
如有疑问,请从在父小组件中管理状态开始。
我们将通过创建三个简单的示例来提供管理状态的不同方法的示例: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
侦听所有点击事件。当用户点击时,它会添加高亮显示(实现为深绿色边框)。当用户松开点击时,它会移除高亮显示。- 在点击、松开点击或取消点击时调用
setState()
来更新 UI,并且_highlight
状态发生更改。 - 在点击事件中,将该状态更改传递给父级小组件,以使用
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
的示例。在 处理点击 中了解有关 GestureDetector
的更多信息,这是 Flutter 烹饪书 中的一道菜谱。
当你需要交互性时,最简单的方法是使用一个预制小组件。以下是部分列表
标准小组件
Material 组件
资源
以下资源可能有助于向你的应用添加交互性。
手势,Flutter 烹饪书 中的一个部分。
- 处理手势
- 如何创建按钮并使其对输入做出响应。
- Flutter 中的手势
- Flutter 手势机制的说明。
- Flutter API 文档
- 所有 Flutter 库的参考文档。
- 奇妙的应用程序 正在运行的应用程序,存储库
- 具有自定义设计和引人入胜的交互的 Flutter 展示应用程序。
- Flutter 的分层设计(视频)
- 此视频包含有关状态和无状态小部件的信息。由 Google 工程师 Ian Hickson 主持。