交互添加

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

你会学到什么

  • 如何响应点击。

  • 如何创建自定义 widget。

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

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

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

The layout tutorial app The 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 管理一行 IconButton 和 Text。

实现一个自定义的有状态 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)

class FavoriteWidget extends StatefulWidget {
  @override
  _FavoriteWidgetState createState() => _FavoriteWidgetState();
}

备忘 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)

class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;
  // ···
}

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

lib/main.dart (_FavoriteWidgetState build)

class _FavoriteWidgetState extends State<FavoriteWidget> {
  // ···
  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: EdgeInsets.all(0),
          child: IconButton(
            icon: (_isFavorited ? Icon(Icons.star) : Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(
          width: 18,
          child: Container(
            child: Text('$_favoriteCount'),
          ),
        ),
      ],
    );
  }
}

小提示 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

void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

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

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

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

| @@ -10,2 +5,2 @@
---|---
105| class 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 state Inactive state

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

widget 管理自己的状态

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

_TapboxAState 类:

  • 管理 TapboxA 的状态.

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

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

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

// TapboxA manages its own state.

// TapboxA 管理自身状态.

//------------------------- TapboxA ----------------------------------

class TapboxA extends StatefulWidget {
  TapboxA({Key key}) : super(key: key);

  @override
  _TapboxAState createState() => _TapboxAState();
}

class _TapboxAState extends State<TapboxA> {
  bool _active = false;

  void _handleTap() {
    setState(() {
      _active = !_active;
    });
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            _active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: _active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

//------------------------- MyApp ----------------------------------

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Flutter Demo'),
        ),
        body: Center(
          child: TapboxA(),
        ),
      ),
    );
  }
}

父 widget 管理 widget 的 state

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

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

ParentWidgetState 类:

  • 为 TapboxB 管理 _active 状态.

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

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

TapboxB 类:

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

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

// ParentWidget manages the state for TapboxB.

// ParentWidget 为 TapboxB 管理状态.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _handleTap,
      child: Container(
        child: Center(
          child: Text(
            active ? 'Active' : 'Inactive',
            style: TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}

小提示 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):

  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 并进行合适的操作。

//---------------------------- ParentWidget ----------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      child: TapboxC(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//----------------------------- TapboxC ------------------------------

class TapboxC extends StatefulWidget {
  TapboxC({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged<bool> onChanged;

  _TapboxCState 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);
  }

  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(
        child: Center(
          child: Text(widget.active ? 'Active' : 'Inactive',
              style: TextStyle(fontSize: 32.0, color: Colors.white)),
        ),
        width: 200.0,
        height: 200.0,
        decoration: BoxDecoration(
          color:
              widget.active ? Colors.lightGreen[700] : Colors.grey[600],
          border: _highlight
              ? Border.all(
                  color: Colors.teal[700],
                  width: 10.0,
                )
              : null,
        ),
      ),
    );
  }
}

另一种实现可能会将高亮状态导出到父 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

质感组件

资源

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