为您的 Flutter 应用添加交互性
如何实现一个响应点击的有状态组件。
如何修改应用以响应用户输入?在本教程中,您将为一个仅包含非交互式组件的应用添加交互性。具体来说,您将通过创建一个管理两个无状态组件的自定义有状态组件,使图标能够响应点击。
构建布局教程向您展示了如何创建以下截图中的布局。
布局教程应用
当应用首次启动时,星形图标是实心的红色,表示该湖泊之前已被收藏。星形旁边的数字表示有 41 个人收藏了这个湖泊。完成本教程后,点击星形图标会取消其收藏状态,将实心星形替换为空心轮廓,并减少计数。再次点击则会收藏该湖泊,显示为实心星形并增加计数。
为实现这一点,您将创建一个单一的自定义组件,其中包含星形和计数,它们本身也是组件。点击星形会改变两个组件的状态,因此同一个组件应该管理两者。
您可以直接跳转到 第 2 步:继承 StatefulWidget 开始编写代码。如果您想尝试不同的状态管理方式,请跳至 状态管理。
有状态(Stateful)和无状态(Stateless)组件
#组件要么是有状态的,要么是无状态的。如果组件可以改变(例如,当用户与其交互时),那么它就是有状态的。
无状态(Stateless)组件从不改变。Icon、IconButton 和 Text 都是无状态组件的示例。无状态组件继承自 StatelessWidget。
有状态(Stateful)组件是动态的:例如,它可以根据用户交互触发的事件或接收数据时改变其外观。Checkbox、Radio、Slider、InkWell、Form 和 TextField 都是有状态组件的示例。有状态组件继承自 StatefulWidget。
组件的状态存储在 State 对象中,将组件的状态与其外观分离开来。状态由可变的值组成,例如滑块的当前值或复选框是否被勾选。当组件的状态改变时,状态对象会调用 setState(),通知框架重新绘制组件。
创建有状态组件
#在本节中,您将创建一个自定义有状态组件。您将用一个单一的自定义有状态组件替换两个无状态组件(实心红星和旁边的数值),该组件管理着包含两个子组件的行:一个 IconButton 和一个 Text。
实现自定义有状态组件需要创建两个类
- 一个
StatefulWidget的子类,用于定义组件。 - 一个
State的子类,包含该组件的状态并定义组件的build()方法。
本节将向您展示如何为湖泊应用构建一个名为 FavoriteWidget 的有状态组件。设置完成后,第一步是选择如何为 FavoriteWidget 管理状态。
第 0 步:准备工作
#如果您已经完成了构建布局教程中的应用,请跳至下一节。
- 确保您已经设置好了您的开发环境。
- 创建一个新的 Flutter 应用.
- 将
lib/main.dart文件替换为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 步:继承 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 上的交互式湖泊示例检查您的代码。
如果您还有疑问,请参考任何一个开发者 社区 频道。
本页面的其余部分介绍了管理组件状态的几种方法,并列出了其他可用的交互式组件。
状态管理
#谁来管理有状态组件的状态?组件本身?父组件?两者?还是另一个对象?答案是……视情况而定。有几种有效的方法可以让您的组件具有交互性。作为组件设计者,您需要根据您预期如何使用组件来做出决定。以下是最常见的管理状态的方法
如何决定使用哪种方法?以下原则应该能帮助您做出决定
-
如果所讨论的状态是用户数据,例如复选框的选中或未选中模式,或者滑块的位置,那么最好由父组件管理该状态。
-
如果所讨论的状态是美观方面的,例如动画,那么最好由组件自身管理该状态。
如有疑问,请先尝试由父组件管理状态。
我们将通过创建三个简单的示例(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 的示例。在 处理点击(Flutter 食谱中的一个方案)中了解有关 GestureDetector 的更多信息。
当您需要交互性时,最简单的方法是使用预制组件之一。以下是部分列表
标准组件
#Material 组件
#资源
#以下资源可能有助于您为应用添加交互性。
手势,Flutter 食谱中的一个章节。
- 处理手势
如何创建按钮并使其响应输入。
- Flutter 中的手势
Flutter 手势机制的描述。
- Flutter API 文档
所有 Flutter 库的参考文档。
- Wonderous 应用 运行应用 , 仓库
具有自定义设计和引人入胜交互的 Flutter 展示应用。
- Flutter 的分层设计(视频)
-
此视频包含有关状态和无状态组件的信息。由谷歌工程师 Ian Hickson 主讲。