跳至主要内容

为您的 Flutter 应用添加交互性

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

构建布局教程中,我们向您展示了如何为以下屏幕截图创建布局。

The layout tutorial app
布局教程应用

当应用程序首次启动时,星星为实心红色,表示该湖泊之前已被收藏。星星旁边的数字表示 41 人收藏了该湖泊。完成本教程后,点击星星将取消其收藏状态,用空心星星替换实心星星并减少计数。再次点击将收藏该湖泊,绘制实心星星并增加计数。

The custom widget you'll create

为了实现这一点,您将创建一个包含星星和计数的单个自定义小部件,它们本身就是小部件。点击星星将更改这两个小部件的状态,因此同一个小部件应该管理这两个小部件。

您可以在步骤 2:子类化 StatefulWidget中直接开始编写代码。如果您想尝试不同的状态管理方式,请跳至管理状态

有状态和小部件

#

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

无状态小部件永远不会改变。IconIconButtonText是无状态小部件的示例。无状态小部件是StatelessWidget的子类。

有状态小部件是动态的:例如,它可以根据用户交互触发的事件或接收数据时改变其外观。CheckboxRadioSliderInkWellFormTextField是有状态小部件的示例。有状态小部件是StatefulWidget的子类。

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

创建有状态的小部件

#

在本节中,您将创建一个自定义有状态小部件。您将用一个管理一行包含两个子小部件(IconButtonText)的自定义有状态小部件替换两个无状态小部件——实心红色星星和它旁边的数字计数。

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

  • 定义小部件的StatefulWidget的子类。
  • 包含该小部件状态并定义小部件build()方法的State的子类。

本节向您展示如何为 lakes 应用程序构建一个名为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),因为IconButton具有一个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'),
          ),
        ),
      ],
    );
  }
  // ···
}

_toggleFavorite()方法(在按下IconButton时调用)调用setState()。调用setState()至关重要,因为这会告诉框架小部件的状态已更改,并且应重新绘制小部件。setState()的函数参数在以下两种状态之间切换 UI

  • 一个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 上的交互式 lakes 示例进行比较。

如果您还有其他问题,请参考任何一个开发者社区渠道。


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

管理状态

#

谁管理有状态小部件的状态?小部件本身?父小部件?两者兼而有之?另一个对象?答案是……视情况而定。有几种有效的方法可以使您的应用交互。作为小部件设计者,您根据预期的小部件使用方式做出决定。以下是管理状态的最常见方法

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

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

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

如有疑问,请先在父小部件中管理状态。

我们将通过创建三个简单的示例: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 Components

#

资源

#

以下资源可能有助于向您的应用添加交互性。

手势,Flutter cookbook 中的一个部分。

处理手势
如何创建按钮并使其响应输入。
Flutter 中的手势
Flutter 手势机制的描述。
Flutter API 文档
所有 Flutter 库的参考文档。
Wonderous 应用 运行应用仓库
具有自定义设计和引人入胜的交互的 Flutter 展示应用。
Flutter 的分层设计(视频)
此视频包含有关状态和无状态部件的信息。由 Google 工程师 Ian Hickson 讲解。