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

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

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 会发生变化,那么它就是 有状态的

无状态的 widget 自身无法改变。 IconIconButtonText 都是无状态 widget,它们都是 StatelessWidget 的子类。

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

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

创建一个有状态的 widget

#

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

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

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

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

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

步骤 0: 开始

#

如果你已经在 构建布局教程 中成功创建了应用程序,你可以跳过下面的部分。

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

  2. 创建一个新的 Flutter 应用

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

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

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

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

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

#

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

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

Step 2: 创建 StatefulWidget 的子类

#

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

dart
class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({super.key});

  @override
  State<FavoriteWidget> createState() => _FavoriteWidgetState();
}

Step 3: 创建 State 的子类

#

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

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

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

dart
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() 是至关重要的,因为这告诉框架, widget 的状态已经改变,应该重绘。 setState() 在如下两种状态中切换 UI:

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

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

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

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

#

将你自定义 stateful widget 在 build() 方法中添加到 widget 树中。首先,找到创建 IconText 的代码,并删除它,在相同的位置创建有状态的 widget:

dart
child: Row(
  children: [
    // ...
    Icon(
      Icons.star,
      color: Colors.red[500],
    ),
    const Text('41'),
    const FavoriteWidget(),
  ],
),

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

有问题?

#

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

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


本页面的其余部分介绍了可以管理 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 的所有交互式行为。

dart
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()),
      ),
    );
  }
}

父 widget 管理 widget 的 state

#

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

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

ParentWidgetState 类:

  • 为 TapboxB 管理 _active 状态;

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

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

TapboxB 类:

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

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

dart
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),
          ),
        ),
      ),
    );
  }
}

混搭管理

#

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

dart
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),
          ),
        ),
      ),
    );
  }
}

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


其他交互式 widgets

#

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

如果你愿意,你可以使用 GestureDetector 来给任何自定义 widget 添加交互性。你可以在 管理状态 中找到 GestureDetector 的示例。同时你也可以在 Flutter 实用教程的 处理点击 中学习更多关于 GestureDetector 的内容。

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

标准 widgets

#

Material 组件

#

资源

#

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

手势,Flutter 实用教程里的一个小节。

处理手势
如何创建一个按钮并使其响应用户动作。

点击、拖动和其他手势
Flutter 手势机制的描述。

Flutter API 文档
所有 Flutter API 的参考文档。

Wonderous app 应用,代码仓库
Flutter 展示应用,采用独特的设计和引人入胜的交互方式。

Flutter 的分层设计 (视频)
此视频包含有关有状态和无状态 widget 的信息。由 Google 工程师 Ian Hickson 讲解。