• 为你的 Flutter 应用加入交互体验
    • 你会学到什么
  • 有状态和无状态的 widgets
  • 创建一个有状态的 widget
    • 重点是什么?
  • 步骤 0: 开始
  • Step 1: 决定哪个对象管理 widget 的状态
  • Step 2: 创建 StatefulWidget 的子类
  • Step 3: 创建 State 的子类
  • Step 4: 将有 stateful widget 插入 widget 树中
  • 有问题?
  • 状态管理
    • 重点是什么?
  • widget 管理自己的状态
  • 父 widget 管理 widget 的 state
  • 混搭管理
  • 其他交互式 widgets
    • 标准 widgets
    • 质感组件
  • 资源

    为你的 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 不会发生变化。Icon、IconButton 和 Text 都是无状态 widget,它们都是 StatelessWidget 的子类。

    stateful widget 是动态的。例如,可以通过与用户的交互或是随着数据的改变而导致外观形态的变化。Checkbox、Radio、Slider、InkWell、Form 和 TextField 都是有状态 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) 中成功创建了应用程序,你可以跳过下面的部分。

    • 确保你已经 设置 好了你的环境.

    • 创建一个基础的 Flutter 应用 —— “Hello World”

    • 用 GitHub 上的 main.dart 替换 lib/main.dart 文件。

    • 用 GitHub 上的 pubspec.yaml 替换 pubspec.yaml 文件。

    • 在你的工程中创建一个 images 文件夹, 并添加 lake.jpg。

    如果你有一个连接并可用的设备,或者你已经启动了 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 上的示例检查代码。

    • lib/main.dart
    • pubspec.yaml
    • lakes.jpg

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


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

    状态管理

    重点是什么?

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

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

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

    谁管理着 stateful widget 的状态?widget 本身?父 widget?双方?另一个对象?答案是……这取决于实际情况。有几种有效的方法可以给你的 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

    • Form
    • FormField

    质感组件

    • Checkbox
    • DropdownButton
    • FlatButton
    • FloatingActionButton
    • IconButton
    • Radio
    • RaisedButton
    • Slider
    • Switch
    • TextField

    资源

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

    • Gestures,Flutter Widget 框架总览 的一节如何创建一个按钮并使其响应用户动作.

    • Gestures in FlutterFlutter 手势机制的描述

    • Flutter API documentation所有 Flutter 库的参考文档.

    • Flutter Gallery一个 Demo 应用程序,展示了许多质感组件和其他 Flutter 功能

    • Flutter’s Layered Design (video)此视频包含有关有状态和无状态 widget 的信息。由 Google 工程师 Ian Hickson 讲解。