跳到主内容

为你的 Flutter 应用添加交互性

如何实现响应点击的有状态组件。

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

构建布局教程》向你展示了如何创建以下屏幕截图的布局。

The layout tutorial app

布局教程应用

当应用首次启动时,星星是纯红色,表明该湖已被收藏。星星旁边的数字表明有 41 人收藏了该湖。完成本教程后,点击星星将移除其收藏状态,将纯星星替换为轮廓并减少计数。再次点击将收藏该湖,绘制纯星星并增加计数。

The custom widget you'll create

为了实现这一点,你将创建一个单一的自定义组件,其中包含星星和计数,它们本身也是组件。点击星星会改变两个组件的状态,因此应该由同一个组件管理两者。

你可以直接进入 步骤 2:继承 StatefulWidget 开始编写代码。如果你想尝试不同的状态管理方式,请跳到 管理状态

有状态和无状态组件

#

一个组件要么是有状态的,要么是无状态的。如果一个组件可以改变——例如,当用户与其交互时——那么它是有状态的。

一个无状态组件永远不会改变。IconIconButtonText 是无状态组件的例子。无状态组件继承自 StatelessWidget

一个有状态组件是动态的:例如,它可以根据用户交互触发的事件或接收到数据时改变其外观。CheckboxRadioSliderInkWellFormTextField 是有状态组件的例子。有状态组件继承自 StatefulWidget

组件的状态存储在一个 State 对象中,将组件的状态与其外观分离。状态由可以改变的值组成,例如滑块的当前值或复选框是否被选中。当组件的状态改变时,状态对象调用 setState(),告诉框架重新绘制组件。

创建有状态组件

#

在本节中,你将创建一个自定义有状态组件。你将用一个单一的自定义有状态组件替换两个无状态组件——纯红色星星和星星旁边的数字计数——该组件管理一个包含两个子组件的行:一个 IconButton 和一个 Text

实现自定义有状态组件需要创建两个类

  • 一个继承自 StatefulWidget 的子类,用于定义组件。
  • 一个继承自 State 的子类,包含该组件的状态并定义组件的 build() 方法。

本节展示了如何为湖泊应用构建一个名为 FavoriteWidget 的有状态组件。设置好后,你的第一步是选择如何管理 FavoriteWidget 的状态。

步骤 0:准备就绪

#

如果你已经在《构建布局教程》中构建了该应用,请跳到下一节。

  1. 确保你已经 设置 了你的环境。
  2. 创建一个新的 Flutter 应用.
  3. main.dart 替换 lib/main.dart 文件。
  4. pubspec.yaml 替换 pubspec.yaml 文件。
  5. 在你的项目中创建一个 images 目录,并添加 lake.jpg

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

步骤 1:确定哪个对象管理组件的状态

#

状态管理有多种方式,但在我们的例子中,组件本身,FavoriteWidget,将管理自己的状态。在本例中,切换星星是一个孤立的操作,不会影响父组件或 UI 的其余部分,因此组件可以内部处理其状态。

更多关于组件和状态分离以及状态管理方式的信息,请参阅 管理状态

步骤 2:继承 StatefulWidget

#

FavoriteWidget 类管理自己的状态,因此它重写 createState() 以创建一个 State 对象。当框架想要构建组件时,会调用 createState()。在本例中,createState() 返回一个 _FavoriteWidgetState 的实例,你将在下一步中实现它。

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

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

步骤 3:继承 State

#

_FavoriteWidgetState 类存储可以在组件生命周期内改变的可变数据。当应用首次启动时,UI 显示一个纯红色星星,表明该湖具有“收藏”状态,以及 41 个点赞。这些值存储在 _isFavorited_favoriteCount 字段中

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

该类还定义了一个 build() 方法,该方法创建一个包含红色 IconButtonText 的行。你使用 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() 至关重要,因为这告诉框架组件的状态已更改,并且应该重新绘制组件。setState() 的函数参数在以下两种状态之间切换

  • 一个 star 图标和数字 41
  • 一个 star_border 图标和数字 40
dart
void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

步骤 4:将有状态组件插入到组件树中

#

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

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

就是这样!当你热重载应用时,星星图标现在应该可以响应点击了。

有问题吗?

#

如果你的代码无法运行,请在你的 IDE 中查找可能的错误。调试 Flutter 应用 可能会有所帮助。如果你仍然找不到问题,请将你的代码与 GitHub 上的交互式湖泊示例进行比较。

如果你仍然有疑问,请参考开发者 社区 的任何一个渠道。


本页的其余部分涵盖了组件状态管理的几种方式,并列出了其他可用的交互式组件。

状态管理

#

谁管理有状态组件的状态?组件本身?父组件?两者?另一个对象?答案是……这取决于。有几种有效的方法可以使你的组件具有交互性。你作为组件设计者根据你期望组件的使用方式做出决定。以下是管理状态的最常见方法

你如何决定使用哪种方法?以下原则应该可以帮助你做出决定

  • 如果相关状态是用户数据,例如复选框的选中或未选中模式,或者滑块的位置,那么最好由父组件管理该状态。

  • 如果相关状态是美观的,例如动画,那么最好由组件本身管理该状态。

如果你不确定,请从在父组件中管理状态开始。

我们将通过创建三个简单的示例:TapboxA、TapboxB 和 TapboxC 来演示不同的状态管理方式。这些示例的工作方式类似——每个示例创建一个容器,当点击时会在绿色或灰色框之间切换。_active 布尔值确定颜色:绿色表示活动状态或灰色表示非活动状态。

Active state Inactive state

这些示例使用 GestureDetector 来捕获 Container 上的活动。

组件管理自己的状态

#

有时,组件自己管理状态是最有意义的。例如,ListView 会在内容超出渲染框时自动滚动。大多数使用 ListView 的开发人员不希望管理 ListView 的滚动行为,因此 ListView 本身管理其滚动偏移量。

_TapboxAState

  • 管理 TapboxA 的状态。
  • 定义 _active 布尔值,该值确定框的当前颜色。
  • 定义 _handleTap() 函数,该函数在点击框时更新 _active 并调用 setState() 函数来更新 UI。
  • 实现组件的所有交互行为。
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()),
      ),
    );
  }
}

父组件管理组件的状态

#

通常,让父组件管理状态并告诉其子组件何时更新是最有意义的。例如,IconButton 允许你将图标视为可点击的按钮。IconButton 是一个无状态组件,因为我们决定父组件需要知道按钮是否被点击,以便它可以采取适当的操作。

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

ParentWidgetState 类

  • 管理 TapboxB_active 状态。
  • 实现 _handleTapboxChanged(),在点击框时调用的方法。
  • 当状态改变时,调用 setState() 来更新 UI。

TapboxB 类

  • 继承自 StatelessWidget,因为所有状态都由其父组件处理。
  • 检测到点击时,它会通知父组件。
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),
          ),
        ),
      ),
    );
  }
}

混合搭配方法

#

对于某些组件,混合搭配方法最有意义。在这种情况下,有状态组件管理部分状态,而父组件管理状态的其他方面。

TapboxC 示例中,按下时,方框周围会出现深绿色的边框。松开时,边框消失,方框的颜色会改变。TapboxC 将其 _active 状态导出到其父组件,但内部管理其 _highlight 状态。此示例有两个 State 对象,分别是 _ParentWidgetState_TapboxCState

_ParentWidgetState 对象

  • 管理 _active 状态。
  • 实现 _handleTapboxChanged(),在点击框时调用的方法。
  • 当发生点击事件且 _active 状态发生变化时,调用 setState() 来更新 UI。

_TapboxCState 对象

  • 管理 _highlight 状态。
  • GestureDetector 监听所有点击事件。当用户按下时,它会添加高亮(实现为深绿色边框)。当用户松开点击时,它会移除高亮。
  • 在按下、松开或取消点击时,如果 _highlight 状态发生变化,则调用 setState() 来更新 UI。
  • 在点击事件发生时,将该状态变化传递给父组件,以便使用 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),
          ),
        ),
      ),
    );
  }
}

另一种实现方式可以将高亮状态导出到父组件,同时将活动状态保留在内部,但如果你要求某人使用该点击框,他们可能会抱怨它没有太大意义。开发者关心方框是否处于活动状态。开发者可能不关心如何管理高亮,并且更喜欢点击框处理这些细节。


其他交互式组件

#

Flutter 提供了各种按钮和类似的交互式组件。其中大多数组件实现了 Material Design 指南,该指南定义了一组具有明确 UI 的组件。

如果你更喜欢,可以使用 GestureDetector 为任何自定义组件构建交互性。你可以在 管理状态 中找到 GestureDetector 的示例。在 Flutter cookbook 的 处理点击 中了解更多关于 GestureDetector 的信息。

当你需要交互性时,使用其中一个预制组件是最简单的。

标准组件

#

Material 组件

#

资源

#

以下资源可能有助于你在应用中添加交互性。

手势,Flutter cookbook 中的一部分。

处理手势

如何创建一个按钮并使其响应输入。

Flutter 中的手势

Flutter 手势机制的描述。

Flutter API 文档

所有 Flutter 库的参考文档。

精彩应用 正在运行的应用仓库

具有自定义设计和引人入胜交互的 Flutter 演示应用。

Flutter 的分层设计 (视频)

此视频包含有关状态和无状态组件的信息。由 Google 工程师 Ian Hickson 讲解。