为你的 Flutter 应用加入交互体验

你会学到什么

  • 如何响应点击。

  • 如何创建自定义 widget。

  • 无状态和有状态 widget 之间的区别。

如何修改您的应用程序以使其对用户输入做出反应?在本教程中,您将为仅包含非交互式 widget 的应用程序添加交互性。具体来说,您将通过创建一个管理两个无状态 widget 的自定义有状态 widget,修改一个图标实现使其可点击。

Layout tutorial 中展示了如何构建下面截图所示的布局。

The layout tutorial appThe layout tutorial app

当应用第一次启动时,这个星形图标是实心红色,表明这个湖以前已经被收藏过了。星号旁边的数字表示 41 个人已经收藏了此湖。完成本教程后,点击星形图标将取消收藏状态,然后用轮廓线的星形图标代替实心的,并减少计数。再次点击会重新收藏,并增加计数。

The custom widget you'll create

为了实现这个,您将创建一个包含星形图标和计数的自定义 widget,它们都是 widget。因为点击星形图标会更改这两个 widget 的状态,所以同一个 widget 应该同时管理这两个 widget。

您可以直接查看 第二步: 创建 StatefulWidget 的子类。如果您想尝试不同的管理状态方式,请跳至 状态管理

有状态和无状态的 widgets

有些 widgets 是有状态的, 有些是无状态的。如果用户与 widget 交互,widget 会发生变化,那么它就是有状态的

Stateless widget 不会发生变化。IconIconButtonText 都是无状态 widget,它们都是 StatelessWidget 的子类。

stateful widget 是动态的。例如,可以通过与用户的交互或是随着数据的改变而导致外观形态的变化。CheckboxRadioSliderInkWellFormTextField 都是有状态 widget,它们都是StatefulWidget 的子类。

一个 widget 的状态保存在一个 State 对象中, 它和 widget 的显示分离。Widget 的状态是一些可以更改的值, 如一个滑动条的当前值或一个复选框是否被选中。当 widget 状态改变时, State 对象调用 setState(), 告诉框架去重绘 widget。

创建一个有状态的 widget

重点是什么?

  • 实现一个有状态 widget 需要创建两个类:一个 StatefulWidget 的子类和一个 State 的子类。

  • State 类包含该 widget 的可变状态并定义该 widget 的 build() 方法.

  • 当 widget 状态改变时, State 对象调用 setState(), 告诉框架去重绘 widget。

在本节中,您将创建一个自定义有状态的 widget。您将使用一个自定义有状态 widget 来替换两个无状态 widget —红色实心星形图标和其旁边的数字计数—该 widget 用两个子 widget 管理一行 IconButtonText

实现一个自定义的有状态 widget 需要创建两个类:

  • 一个 StatefulWidget 的子类,用来定义一个 widget 类。

  • 一个 State 的子类,包含该widget状态并定义该 widget 的 build() 方法.

这一节展示如何为 Lakes 应用程序构建一个名为 FavoriteWidget 的 StatefulWidget。第一步是选择如何管理 FavoriteWidget 的状态。

步骤 0: 开始

如果你已经在 Layout tutorial (step 6) 中成功创建了应用程序,你可以跳过下面的部分。

如果你有一个连接并可用的设备,或者你已经启动了 iOS simulator(Flutter 安装部分介绍过),你就可以开始了!

Step 1: 决定哪个对象管理 widget 的状态

一个 widget 的状态可以通过多种方式进行管理,但在我们的示例中,widget 本身,FavoriteWidget,将管理自己的状态。在这个例子中,切换星形图标是一个独立的操作,不会影响父窗口 widget 或其他用户界面,因此该 widget 可以在内部处理它自己的状态。

你可以在 状态管理 中了解更多关于 widget 和状态的分离以及如何管理状态的信息。

Step 2: 创建 StatefulWidget 的子类

FavoriteWidget 类管理自己的状态,因此它通过重写 createState() 来创建状态对象。框架会在构建 widget 时调用 createState()。在这个例子中,createState() 创建 _FavoriteWidgetState 的实例,您将在下一步中实现该实例。

lib/main.dart (FavoriteWidget)

  1. class FavoriteWidget extends StatefulWidget {
  2. @override
  3. _FavoriteWidgetState createState() => _FavoriteWidgetState();
  4. }

备忘 Members or classes that start with an underscore (_) are private. For more information, see Libraries and visibility, a section in the Dart language tour.

以下划线(_)开头的成员或类是私有的。有关更多信息,请参阅 Dart language tour 中的 Libraries and visibility 部分。

Step 3: 创建 State 的子类

_FavoriteWidgetState 类存储可变信息;可以在 widget 的生命周期内改变逻辑和内部状态。当应用第一次启动时,用户界面显示一个红色实心的星星形图标,表明该湖已经被收藏,并有 41 个“喜欢”。状态对象存储这些信息在 _isFavorited_favoriteCount 变量中。

lib/main.dart (_FavoriteWidgetState fields)

  1. class _FavoriteWidgetState extends State<FavoriteWidget> {
  2. bool _isFavorited = true;
  3. int _favoriteCount = 41;
  4. // ···
  5. }

状态对象也定义了 build() 方法。这个 build() 方法创建一个包含红色 IconButtonText 的行。该 widget 使用 IconButton(而不是 Icon),因为它具有一个 onPressed 属性,该属性定义了处理点击的回调方法(_toggleFavorite)。你将会在接下来的步骤中尝试定义它。

lib/main.dart (_FavoriteWidgetState build)

  1. class _FavoriteWidgetState extends State<FavoriteWidget> {
  2. // ···
  3. @override
  4. Widget build(BuildContext context) {
  5. return Row(
  6. mainAxisSize: MainAxisSize.min,
  7. children: [
  8. Container(
  9. padding: EdgeInsets.all(0),
  10. child: IconButton(
  11. icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
  12. color: Colors.red[500],
  13. onPressed: _toggleFavorite,
  14. ),
  15. ),
  16. SizedBox(
  17. width: 18,
  18. child: Container(
  19. child: Text('$_favoriteCount'),
  20. ),
  21. ),
  22. ],
  23. );
  24. }
  25. }

小提示 Placing the Text in a SizedBox and setting its width prevents a discernible “jump” when the text changes between the values of 40 and 41 — a jump would otherwise occur because those values have different widths.

Text 在 40 和 41 之间变化时,将文本放在 SizedBox 中并设置其宽度可防止出现明显的“跳跃”,因为这些值具有不同的宽度。

按下 IconButton 时会调用 _toggleFavorite() 方法,然后它会调用 setState()。调用 setState() 是至关重要的,因为这告诉框架,widget 的状态已经改变,应该重绘。setState() 在如下两种状态中切换 UI:

  • 实心的星形图标和数字 ‘41’

  • 轮廓线的星形图标和数字 ‘40’ 之间切换 UI

  1. void _toggleFavorite() {
  2. setState(() {
  3. if (_isFavorited) {
  4. _favoriteCount -= 1;
  5. _isFavorited = false;
  6. } else {
  7. _favoriteCount += 1;
  8. _isFavorited = true;
  9. }
  10. });
  11. }

Step 4: 将有 stateful widget 插入 widget 树中

将您自定义 stateful widget 在 build() 方法中添加到 widget 树中。首先,找到创建图标文本的代码,并删除它,在相同的位置创建 stateful widget:

交互添加 - 图3layout/lakes/{step6 → interactive}/lib/main.dart

@@ -10,2 +5,2 @@
105class MyApp extends StatelessWidget {
116 @override
@@ -38,11 +33,7 @@
3833 ],
3934 ),
4035 ),
41-Icon(
36+FavoriteWidget(),
42- Icons.star,
43- color: Colors.red[500],
44- ),
45- Text('41'),
4637 ],
4738 ),
4839 );
@@ -117,2 +108,2 @@
117108 );
118109 }

就是这样!当您热重载应用后,星形图标就会响应点击了.

有问题?

如果您的代码无法运行,请在 IDE 中查找可能的错误。调试 Flutter 应用程序 可能会有所帮助。如果仍然无法找到问题,请根据 GitHub 上的示例检查代码。

如果您仍有问题, 可以咨询 社区 中的任何一位开发者。


本页面的其余部分介绍了可以管理 widget 状态的几种方式,并列出了其他可用的可交互的 widget。

状态管理

重点是什么?

  • 有多种方法可以管理状态。

  • 您作为 widget 的设计者,需要选择使用何种管理方法。

  • 如果不是很清楚时, 就在父 widget 中管理状态。

谁管理着 stateful widget 的状态?widget 本身?父 widget?双方?另一个对象?答案是……这取决于实际情况。有几种有效的方法可以给你的 widget 添加互动。作为 widget 设计师,你可以基于你所期待的表现 widget 的方式来做决定。以下是一些管理状态的最常见的方法:

如何决定使用哪种管理方法?以下原则可以帮助您决定:

  • 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父 widget 管理。

  • 如果所讨论的状态是有关界面外观效果的,例如动画,那么状态最好由 widget 本身来管理。

如果有疑问,首选是在父 widget 中管理状态。

我们将通过创建三个简单示例来举例说明管理状态的不同方式:TapboxA、TapboxB 和 TapboxC。这些例子功能是相似的 - 每创建一个容器,当点击时,在绿色或灰色框之间切换。_active 确定颜色:绿色为 true,灰色为 false。

Active stateInactive state

这些示例使用 GestureDetector 捕获 Container 上的用户动作。

widget 管理自己的状态

有时,widget 在内部管理其状态是最好的。例如,当 ListView 的内容超过渲染框时,ListView 自动滚动。大多数使用 ListView 的开发人员不想管理 ListView 的滚动行为,因此 ListView 本身管理其滚动偏移量。

_TapboxAState 类:

  • 管理 TapboxA 的状态.

  • 定义布尔值 _active 确定盒子的当前颜色.

  • 定义 _handleTap() 函数,该函数在点击该盒子时更新 _active,并调用 setState() 更新 UI。

  • 实现 widget 的所有交互式行为.

  1. // TapboxA manages its own state.
  2.  
  3. // TapboxA 管理自身状态.
  4.  
  5. //------------------------- TapboxA ----------------------------------
  6.  
  7. class TapboxA extends StatefulWidget {
  8. TapboxA({Key key}) : super(key: key);
  9.  
  10. @override
  11. _TapboxAState createState() => _TapboxAState();
  12. }
  13.  
  14. class _TapboxAState extends State<TapboxA> {
  15. bool _active = false;
  16.  
  17. void _handleTap() {
  18. setState(() {
  19. _active = !_active;
  20. });
  21. }
  22.  
  23. Widget build(BuildContext context) {
  24. return GestureDetector(
  25. onTap: _handleTap,
  26. child: Container(
  27. child: Center(
  28. child: Text(
  29. _active ? 'Active' : 'Inactive',
  30. style: TextStyle(fontSize: 32.0, color: Colors.white),
  31. ),
  32. ),
  33. width: 200.0,
  34. height: 200.0,
  35. decoration: BoxDecoration(
  36. color: _active ? Colors.lightGreen[700] : Colors.grey[600],
  37. ),
  38. ),
  39. );
  40. }
  41. }
  42.  
  43. //------------------------- MyApp ----------------------------------
  44.  
  45. class MyApp extends StatelessWidget {
  46. @override
  47. Widget build(BuildContext context) {
  48. return MaterialApp(
  49. title: 'Flutter Demo',
  50. home: Scaffold(
  51. appBar: AppBar(
  52. title: Text('Flutter Demo'),
  53. ),
  54. body: Center(
  55. child: TapboxA(),
  56. ),
  57. ),
  58. );
  59. }
  60. }

父 widget 管理 widget 的 state

一般来说父 widget 管理状态并告诉其子 widget 何时更新通常是最合适的。例如,IconButton 允许您将图标视为可点按的按钮。IconButton 是一个无状态的小部件,因为我们认为父 widget 需要知道该按钮是否被点击来采取相应的处理。

在以下示例中,TapboxB 通过回调将其状态到其父类。由于 TapboxB 不管理任何状态,因此它继承自 StatelessWidget。

ParentWidgetState 类:

  • 为 TapboxB 管理 _active 状态.

  • 实现 _handleTapboxChanged(),当盒子被点击时调用的方法.

  • 当状态改变时,调用 setState() 更新 UI.

TapboxB 类:

  • 继承 StatelessWidget 类,因为所有状态都由其父 widget 处理.

  • 当检测到点击时,它会通知父 widget.

  1. // ParentWidget manages the state for TapboxB.
  2.  
  3. // ParentWidget 为 TapboxB 管理状态.
  4.  
  5. //------------------------ ParentWidget --------------------------------
  6.  
  7. class ParentWidget extends StatefulWidget {
  8. @override
  9. _ParentWidgetState createState() => _ParentWidgetState();
  10. }
  11.  
  12. class _ParentWidgetState extends State<ParentWidget> {
  13. bool _active = false;
  14.  
  15. void _handleTapboxChanged(bool newValue) {
  16. setState(() {
  17. _active = newValue;
  18. });
  19. }
  20.  
  21. @override
  22. Widget build(BuildContext context) {
  23. return Container(
  24. child: TapboxB(
  25. active: _active,
  26. onChanged: _handleTapboxChanged,
  27. ),
  28. );
  29. }
  30. }
  31.  
  32. //------------------------- TapboxB ----------------------------------
  33.  
  34. class TapboxB extends StatelessWidget {
  35. TapboxB({Key key, this.active: false, @required this.onChanged})
  36. : super(key: key);
  37.  
  38. final bool active;
  39. final ValueChanged<bool> onChanged;
  40.  
  41. void _handleTap() {
  42. onChanged(!active);
  43. }
  44.  
  45. Widget build(BuildContext context) {
  46. return GestureDetector(
  47. onTap: _handleTap,
  48. child: Container(
  49. child: Center(
  50. child: Text(
  51. active ? 'Active' : 'Inactive',
  52. style: TextStyle(fontSize: 32.0, color: Colors.white),
  53. ),
  54. ),
  55. width: 200.0,
  56. height: 200.0,
  57. decoration: BoxDecoration(
  58. color: active ? Colors.lightGreen[700] : Colors.grey[600],
  59. ),
  60. ),
  61. );
  62. }
  63. }

小提示 When creating API, consider using the @required annotation for any parameters that your code relies on. To use @required, import the foundation library (which re-exports Dart’s meta.dart library):

在创建 API 时,请考虑使用 @required 为代码所依赖的任何参数使用注解。要使用 @required 注解,请导入 foundation library(该库重新导出 Dart 的 meta.dart):

  1. import 'package:flutter/foundation.dart';

混搭管理

对于一些 widget 来说,混搭管理的方法最合适的。在这种情况下,有状态的 widget 自己管理一些状态,同时父 widget 管理其他方面的状态。

TapboxC 示例中,点击时,盒子的周围会出现一个深绿色的边框。点击时,边框消失,盒子的颜色改变。TapboxC 将其 _active 状态导出到其父 widget 中,但在内部管理其 _highlight 状态。这个例子有两个状态对象 _ParentWidgetState_TapboxCState

_ParentWidgetState 对象:

  • 管理_active 状态。

  • 实现 _handleTapboxChanged(), 此方法在盒子被点击时调用。

  • 当点击盒子并且 _active 状态改变时调用 setState() 来更新UI。

_TapboxCState 对象:

  • 管理 _highlight state。

  • GestureDetector 监听所有 tap 事件。当用户点下时,它添加高亮(深绿色边框);当用户释放时,会移除高亮。

  • 当按下、抬起、或者取消点击时更新 _highlight 状态,调用 setState() 更新UI。

  • 当点击时,widget 属性将状态的改变传递给父 widget 并进行合适的操作。

  1. //---------------------------- ParentWidget ----------------------------
  2.  
  3. class ParentWidget extends StatefulWidget {
  4. @override
  5. _ParentWidgetState createState() => _ParentWidgetState();
  6. }
  7.  
  8. class _ParentWidgetState extends State<ParentWidget> {
  9. bool _active = false;
  10.  
  11. void _handleTapboxChanged(bool newValue) {
  12. setState(() {
  13. _active = newValue;
  14. });
  15. }
  16.  
  17. @override
  18. Widget build(BuildContext context) {
  19. return Container(
  20. child: TapboxC(
  21. active: _active,
  22. onChanged: _handleTapboxChanged,
  23. ),
  24. );
  25. }
  26. }
  27.  
  28. //----------------------------- TapboxC ------------------------------
  29.  
  30. class TapboxC extends StatefulWidget {
  31. TapboxC({Key key, this.active: false, @required this.onChanged})
  32. : super(key: key);
  33.  
  34. final bool active;
  35. final ValueChanged<bool> onChanged;
  36.  
  37. _TapboxCState createState() => _TapboxCState();
  38. }
  39.  
  40. class _TapboxCState extends State<TapboxC> {
  41. bool _highlight = false;
  42.  
  43. void _handleTapDown(TapDownDetails details) {
  44. setState(() {
  45. _highlight = true;
  46. });
  47. }
  48.  
  49. void _handleTapUp(TapUpDetails details) {
  50. setState(() {
  51. _highlight = false;
  52. });
  53. }
  54.  
  55. void _handleTapCancel() {
  56. setState(() {
  57. _highlight = false;
  58. });
  59. }
  60.  
  61. void _handleTap() {
  62. widget.onChanged(!widget.active);
  63. }
  64.  
  65. Widget build(BuildContext context) {
  66. // This example adds a green border on tap down.
  67. // On tap up, the square changes to the opposite state.
  68. return GestureDetector(
  69. onTapDown: _handleTapDown, // Handle the tap events in the order that
  70. onTapUp: _handleTapUp, // they occur: down, up, tap, cancel
  71. onTap: _handleTap,
  72. onTapCancel: _handleTapCancel,
  73. child: Container(
  74. child: Center(
  75. child: Text(widget.active ? 'Active' : 'Inactive',
  76. style: TextStyle(fontSize: 32.0, color: Colors.white)),
  77. ),
  78. width: 200.0,
  79. height: 200.0,
  80. decoration: BoxDecoration(
  81. color:
  82. widget.active ? Colors.lightGreen[700] : Colors.grey[600],
  83. border: _highlight
  84. ? Border.all(
  85. color: Colors.teal[700],
  86. width: 10.0,
  87. )
  88. : null,
  89. ),
  90. ),
  91. );
  92. }
  93. }

另一种实现可能会将高亮状态导出到父 widget,同时保持 active 状态为内部,但如果您要求某人使用该 TapBox,他们可能会抱怨说没有多大意义。开发人员只会关心该框是否处于活动状态。开发人员可能不在乎高亮显示是如何管理的,并且倾向于让 TapBox 处理这些细节。


其他交互式 widgets

Flutter 提供各种按钮和类似的交互式 widget。这些 widget 中的大多数都实现了 Material Design guidelines,它们定义了一组具有质感的 UI 组件。

如果你愿意,你可以使用 GestureDetector 来给任何自定义 widget 添加交互性。您可以在 管理状态Flutter Gallery 中找到 GestureDetector 的示例。

小提示 Flutter also provides a set of iOS-style widgets called Cupertino.

Futter还提供了一组名为 Cupertino 的 iOS 风格的小部件。

当你需要交互性时,最容易的是使用预制的 widget。这是预置 widget 部分列表:

标准 widgets

质感组件

资源

以下资源可能会在给您的应用添加交互的时候有所帮助。