Skip to main content

为你的Flutter应用添加交互性

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

构建布局教程 向你展示了如何为以下截图创建布局。

布局教程应用
布局教程应用

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

你将创建的自定义widget

为此,你将创建一个包含星星和计数的单个自定义widget, 它们本身就是widget。点击星星会更改这两个widget的状态,因此同一个widget应该管理两者。

你可以在 步骤2:子类化StatefulWidget中直接接触代码。 如果你想尝试不同的状态管理方法, 请跳到状态管理

有状态和无状态widget

#

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

一个_无状态_ widget永远不会改变。 IconIconButtonText是无状态widget的示例。无状态widget是StatelessWidget的子类。

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

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

创建一个有状态widget

#

在本节中,你将创建一个自定义有状态widget。 你将用一个管理一行包含两个子widget的单个自定义有状态widget替换两个无状态widget——实心红星及其旁边的数字计数:一个IconButtonText

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

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

本节将向你展示如何为lakes应用构建一个名为FavoriteWidget的有状态widget。 设置完成后,你的第一步是选择如何管理FavoriteWidget的状态。

步骤0:准备工作

#

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

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

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

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

#

widget的状态可以通过几种方式管理, 但在我们的示例中,widget本身, FavoriteWidget,将管理它自己的状态。 在这个例子中,切换星星是一个隔离的动作,不会影响父widget或UI的其余部分,因此widget可以在内部处理其状态。

状态管理中,了解更多关于widget和状态的分离,以及如何管理状态。

步骤2:子类化StatefulWidget

#

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

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

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

步骤3:子类化State

#

_FavoriteWidgetState类存储在widget生命周期中可能发生更改的可变数据。 当应用程序首次启动时,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'),
          ),
        ),
      ],
    );
  }
  // ···
}

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

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

步骤4:将有状态widget插入widget树

#

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

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

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

问题?

#

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

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


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

状态管理

#

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

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

  • 如果相关状态是用户数据, 例如选中或未选中

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

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

如有疑问,请从在父widget中管理状态开始。

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

活动状态 非活动状态

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

widget自身管理其状态

#

有时,让widget在内部管理其状态是最有意义的。例如,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的状态

#

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

在下面的例子中,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),
          ),
        ),
      ),
    );
  }
}

混合搭配的方法

#

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

TapboxC示例中,按下时,方块周围会出现一个深绿色的边框。抬起时,边框消失,方块的颜色发生变化。TapboxC将其_active状态导出到其父级,但内部管理其_highlight状态。此示例有两个State对象,_ParentWidgetState_TapboxCState

_ParentWidgetState对象:

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

_TapboxCState对象:

  • 管理_highlight状态。
  • GestureDetector监听所有点击事件。当用户按下时,它会添加高亮显示(实现为深绿色边框)。当用户释放点击时,它会删除高亮显示。
  • 在按下、抬起或取消点击以及_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

#

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

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

当你需要交互性时,使用预制widget最简单。这是一个部分列表:

标准widget

#

Material组件

#

资源

#

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

手势Flutter cookbook中的一个部分。

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