Skip to main content

编写你的第一个 Flutter Web 应用

您将构建的 Web 应用。

这是一个创建您的第一个 Flutter Web 应用的指南。如果您熟悉面向对象编程以及变量、循环和条件语句等概念,则可以完成本教程。您不需要具备 Dart、移动或 Web 编程的先前经验。

您将构建的内容

#

您将实现一个简单的 Web 应用,该应用显示登录屏幕。屏幕包含三个文本字段:名字、姓氏和用户名。当用户填写这些字段时,进度条会在登录区域顶部进行动画显示。当三个字段都填写完毕时,进度条会以绿色显示在登录区域的整个宽度上,并且 注册 按钮将启用。单击 注册 按钮会导致欢迎屏幕从屏幕底部进行动画显示。

动画 GIF 显示了完成本实验后应用的工作方式。

步骤 0:获取入门 Web 应用

#

您将从我们为您提供的简单 Web 应用开始。

  1. 启用 Web 开发。
    在命令行中,执行以下命令以确保您已正确安装 Flutter。
    flutter doctor
    Doctor summary (to see all details, run flutter doctor -v):
    [✓] Flutter (Channel stable, 3.27.0, on macOS darwin-arm64, locale en)
    [✓] Android toolchain - develop for Android devices (Android SDK version 35.0.1)
    [✓] Xcode - develop for iOS and macOS (Xcode 16)
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2024.2)
    [✓] VS Code (version 1.95)
    [✓] Connected device (4 available)
    [✓] HTTP Host Availability
    
    • No issues found!

    如果您看到“flutter: command not found”,则请确保您已安装Flutter SDK 并将其添加到您的 PATH 中。

    如果 Android 工具链、Android Studio 和 Xcode 工具未安装,也没有关系,因为该应用仅供 Web 使用。如果您以后希望此应用可在移动设备上运行,则需要进行额外的安装和设置。

  2. 列出设备。
    为了确保已安装 Web,请列出可用的设备。您应该会看到类似以下内容:

    flutter devices
    4 connected devices:
    
    sdk gphone64 arm64 (mobile) • emulator-5554                        •
    android-arm64  • Android 13 (API 33) (emulator)
    iPhone 14 Pro Max (mobile)  • 45A72BE1-2D4E-4202-9BB3-D6AE2601BEF8 • ios
    • com.apple.CoreSimulator.SimRuntime.iOS-16-0 (simulator)
    macOS (desktop)             • macos                                •
    darwin-arm64   • macOS 12.6 21G115 darwin-arm64
    Chrome (web)                • chrome                               •
    web-javascript • Google Chrome 105.0.5195.125

    Chrome 设备会自动启动 Chrome 并启用 Flutter DevTools 工具的使用。

  3. 起始应用显示在下面的 DartPad 中。

    import 'package:flutter/material.dart';
    
    void main() => runApp(const SignUpApp());
    
    class SignUpApp extends StatelessWidget {
      const SignUpApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          routes: {
            '/': (context) => const SignUpScreen(),
          },
        );
      }
    }
    
    class SignUpScreen extends StatelessWidget {
      const SignUpScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.grey[200],
          body: const Center(
            child: SizedBox(
              width: 400,
              child: Card(
                child: SignUpForm(),
              ),
            ),
          ),
        );
      }
    }
    
    class SignUpForm extends StatefulWidget {
      const SignUpForm({super.key});
    
      @override
      State<SignUpForm> createState() => _SignUpFormState();
    }
    
    class _SignUpFormState extends State<SignUpForm> {
      final _firstNameTextController = TextEditingController();
      final _lastNameTextController = TextEditingController();
      final _usernameTextController = TextEditingController();
    
      double _formProgress = 0;
    
      @override
      Widget build(BuildContext context) {
        return Form(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              LinearProgressIndicator(value: _formProgress),
              Text('Sign up', style: Theme.of(context).textTheme.headlineMedium),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _firstNameTextController,
                  decoration: const InputDecoration(hintText: 'First name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _lastNameTextController,
                  decoration: const InputDecoration(hintText: 'Last name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _usernameTextController,
                  decoration: const InputDecoration(hintText: 'Username'),
                ),
              ),
              TextButton(
                style: ButtonStyle(
                  foregroundColor: WidgetStateProperty.resolveWith((states) {
                    return states.contains(WidgetState.disabled)
                        ? null
                        : Colors.white;
                  }),
                  backgroundColor: WidgetStateProperty.resolveWith((states) {
                    return states.contains(WidgetState.disabled)
                        ? null
                        : Colors.blue;
                  }),
                ),
                onPressed: null,
                child: const Text('Sign up'),
              ),
            ],
          ),
        );
      }
    }
    
  4. 运行示例。
    单击 运行 按钮以运行示例。请注意,您可以输入文本字段,但 注册 按钮处于禁用状态。

  5. 复制代码。
    单击代码窗格右上角的剪贴板图标,将 Dart 代码复制到剪贴板。

  6. 创建一个新的 Flutter 项目。
    在您的 IDE、编辑器或命令行中,创建一个新的 Flutter 项目,并将其命名为signin_example

  7. lib/main.dart 的内容替换为剪贴板中的内容。

观察结果

#
  • 此示例的全部代码都位于 lib/main.dart 文件中。
  • 如果你了解 Java,Dart 语言应该感觉非常熟悉。
  • 所有应用的 UI 都是用 Dart 代码创建的。 更多信息,请参见声明式 UI 简介
  • 应用的 UI 遵循Material Design,这是一种可在任何设备或平台上运行的可视化设计语言。您可以自定义 Material Design 小部件,但如果您更喜欢其他内容,Flutter 还提供 Cupertino 小部件库,它实现了当前的 iOS 设计语言。或者您可以创建自己的自定义小部件库。
  • 在 Flutter 中,几乎所有东西都是小部件。即使应用本身也是一个小部件。应用的 UI 可以描述为一个小部件树。

步骤 1:显示欢迎屏幕

#

SignUpForm 类是有状态的小部件。这仅仅意味着小部件存储可以更改的信息,例如用户输入或来自 Feed 的数据。由于小部件本身是不可变的(一旦创建就不能修改),Flutter 将状态信息存储在一个配套类中,称为 State 类。在本实验中,您所有编辑都将对私有的 _SignUpFormState 类进行。

首先,在您的 lib/main.dart 文件中,在 SignUpScreen 类之后添加以下 WelcomeScreen 小部件的类定义:

dart
class WelcomeScreen extends StatelessWidget {
  const WelcomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text(
          'Welcome!',
          style: Theme.of(context).textTheme.displayMedium,
        ),
      ),
    );
  }
}

接下来,您将启用按钮以显示屏幕并创建显示它的方法。

  1. 找到 _SignUpFormState 类的 build() 方法。这是构建“注册”按钮的代码部分。请注意按钮是如何定义的:它是一个带有蓝色背景的 TextButton,带有显示为 注册 的白色文本,并且按下时什么也不做。

  2. 更新 onPressed 属性。
    onPressed 属性更改为调用将显示欢迎屏幕的(不存在的)方法。

    onPressed: null 更改为以下内容:

    dart
    onPressed: _showWelcomeScreen,
  3. 添加 _showWelcomeScreen 方法。
    修复分析器报告的 _showWelcomeScreen 未定义的错误。在 build() 方法正上方,添加以下函数:

    dart
    void _showWelcomeScreen() {
      Navigator.of(context).pushNamed('/welcome');
    }
  4. 添加 /welcome 路由。
    创建连接以显示新屏幕。在 SignUpAppbuild() 方法中,在 '/' 下添加以下路由:

    dart
    '/welcome': (context) => const WelcomeScreen(),
  5. 运行应用。
    注册按钮现在应该已启用。单击它以调出欢迎屏幕。请注意它是如何从底部进行动画显示的。您免费获得了这种行为。

观察结果

#
  • _showWelcomeScreen() 函数在 build() 方法中用作回调函数。回调函数经常在 Dart 代码中使用,在这种情况下,这意味着“按下按钮时调用此方法”。
  • 构造函数前面的 const 关键字非常重要。当 Flutter 遇到一个常量小部件时,它会简化大多数底层重建工作,从而提高渲染效率。
  • Flutter 只有一个 Navigator 对象。此小部件在一个堆栈内管理 Flutter 的屏幕(也称为 路由 或_页面_)。堆栈顶部的屏幕是当前显示的视图。将新屏幕推送到此堆栈会将显示切换到该新屏幕。这就是 _showWelcomeScreen 函数将 WelcomeScreen 推送到 Navigator 堆栈的原因。用户单击按钮,瞧,欢迎屏幕出现了。同样,在 Navigator 上调用 pop() 会返回到上一屏幕。因为 Flutter 的导航已集成到浏览器的导航中,所以在单击浏览器的后退箭头按钮时,这会隐式发生。

步骤 2:启用登录进度跟踪

#

此登录屏幕包含三个字段。接下来,您将启用跟踪用户填写表单字段的进度,并在表单填写完毕时更新应用的 UI。

  1. 添加一个更新 _formProgress 的方法。 在 _SignUpFormState 类中,添加一个名为 _updateFormProgress() 的新方法:

    dart
    void _updateFormProgress() {
      var progress = 0.0;
      final controllers = [
        _firstNameTextController,
        _lastNameTextController,
        _usernameTextController
      ];
    
      for (final controller in controllers) {
        if (controller.value.text.isNotEmpty) {
          progress += 1 / controllers.length;
        }
      }
    
      setState(() {
        _formProgress = progress;
      });
    }

    此方法根据非空文本字段的数量更新 _formProgress 字段。

  2. 当表单发生更改时调用 _updateFormProgress
    _SignUpFormState 类的 build() 方法中,向 Form 小部件的 onChanged 参数添加回调。在下面标记为 NEW 的位置添加代码:

    dart
    return Form(
      onChanged: _updateFormProgress, // NEW
      child: Column(
  3. 更新 onPressed 属性(再次)。
    在步骤 1 中,您修改了 注册 按钮的 onPressed 属性以显示欢迎屏幕。现在,更新该按钮,使其仅在表单完全填写完毕时才显示欢迎屏幕:

    dart
    TextButton(
      style: ButtonStyle(
        foregroundColor: WidgetStateProperty.resolveWith((states) {
          return states.contains(WidgetState.disabled)
              ? null
              : Colors.white;
        }),
        backgroundColor: WidgetStateProperty.resolveWith((states) {
          return states.contains(WidgetState.disabled)
              ? null
              : Colors.blue;
        }),
      ),
      onPressed:
          _formProgress == 1 ? _showWelcomeScreen : null, // UPDATED
      child: const Text('Sign up'),
    ),
  4. 运行应用。
    注册按钮最初处于禁用状态,但当所有三个文本字段包含(任何)文本时,它将启用。

观察结果

#
  • 调用小部件的 setState() 方法会告诉 Flutter 需要在屏幕上更新小部件。然后,框架会处理掉以前的不变小部件(及其子部件),创建一个新的(及其伴随的子小部件树),并将其渲染到屏幕上。为了使这能无缝工作,Flutter 需要很快。新的部件树必须在不到 1/60 秒的时间内创建并渲染到屏幕上,才能创建一个流畅的视觉转换——尤其是在动画中。幸运的是,Flutter 确实很快。

  • progress 字段定义为浮点值,并在 _updateFormProgress 方法中更新。当所有三个字段都填写完毕时,_formProgress 将设置为 1.0。当 _formProgress 设置为 1.0 时,onPressed 回调将设置为 _showWelcomeScreen 方法。现在它的 onPressed 参数非空,按钮已启用。与 Flutter 中的大多数 Material Design 按钮一样,如果 TextButtononPressedonLongPress 回调为 null,则默认情况下它们处于禁用状态。

  • 请注意,_updateFormProgresssetState() 传递了一个函数。这被称为匿名函数,其语法如下:

    dart
    methodName(() {...});

    其中 methodName 是一个命名函数,它接受一个匿名回调函数作为参数。

  • 上一步中显示欢迎屏幕的 Dart 语法是:

    dart
    _formProgress == 1 ? _showWelcomeScreen : null

    这是一个 Dart 条件赋值,其语法为:condition ? expression1 : expression2。如果表达式 _formProgress == 1 为真,则整个表达式的结果为 : 左侧的值,在本例中为 _showWelcomeScreen 方法。

步骤 2.5:启动 Dart DevTools

#

如何调试 Flutter Web 应用?它与调试任何 Flutter 应用没有什么不同。您需要使用Dart DevTools!(不要与 Chrome DevTools 混淆。)

我们的应用目前没有错误,但无论如何让我们检查一下。启动 DevTools 的以下说明适用于任何工作流程,但如果您使用 IntelliJ,则有一个快捷方式。有关更多信息,请参阅本节末尾的提示。

  1. 运行应用。
    如果您的应用当前未运行,请启动它。从下拉菜单中选择Chrome设备,并从您的 IDE 启动它,或者从命令行使用 flutter run -d chrome

  2. 获取 DevTools 的 Websocket 信息。
    在命令行或 IDE 中,您应该会看到一条消息,类似如下所示:

    Launching lib/main.dart on Chrome in debug mode...
    Building application for the web...                                11.7s
    Attempting to connect to browser instance..
    Debug service listening on <b>ws://127.0.0.1:54998/pJqWWxNv92s=</b>

    复制调试服务的地址(以粗体显示)。您需要它来启动 DevTools。

  3. 确保已安装 Dart 和 Flutter 插件。
    如果您使用的是 IDE,请确保已设置 Flutter 和 Dart 插件,如VS CodeAndroid Studio 和 IntelliJ 页面中所述。如果您在命令行中工作,请按照DevTools 命令行 页面中的说明启动 DevTools 服务器。

  4. 连接到 DevTools。
    当 DevTools 启动时,您应该会看到类似以下内容:

    Serving DevTools at http://127.0.0.1:9100

    在 Chrome 浏览器中访问此 URL。您应该会看到 DevTools 启动屏幕。它应该如下所示:

    DevTools 启动屏幕的屏幕截图

  5. 连接到正在运行的应用。
    连接到正在运行的站点 下,粘贴您在步骤 2 中复制的 Websocket (ws) 位置,然后单击 连接 。您现在应该会看到 Dart DevTools 在您的 Chrome 浏览器中成功运行:

    DevTools 运行屏幕的屏幕截图

    恭喜,您现在正在运行 Dart DevTools!

  1. 设置断点。
    现在您已运行 DevTools,请在顶部的蓝色栏中选择 调试器 选项卡。调试器窗格出现,在左下方,可以看到示例中使用的库列表。选择 lib/main.dart 以在中心窗格中显示您的 Dart 代码。

    DevTools 调试器的屏幕截图

  2. 设置断点。
    在 Dart 代码中,向下滚动到更新 progress 的位置:

    dart
    for (final controller in controllers) {
      if (controller.value.text.isNotEmpty) {
        progress += 1 / controllers.length;
      }
    }

    通过单击行号左侧来在包含 for 循环的行上设置断点。断点现在出现在窗口左侧的 断点 部分中。

  3. 触发断点。
    在正在运行的应用中,单击其中一个文本字段以获取焦点。应用会命中断点并暂停。在 DevTools 屏幕中,您可以在左侧看到 progress 的值,为 0。这是可以预料的,因为没有字段被填写。单步执行 for 循环以查看程序执行情况。

  4. 恢复应用。
    通过单击 DevTools 窗口中的绿色 恢复 按钮来恢复应用。

  5. 删除断点。
    再次单击断点将其删除,然后恢复应用。

这使您对使用 DevTools 可以实现的功能有了小小的了解,但还有更多功能!有关更多信息,请参阅DevTools 文档

步骤 3:为登录进度添加动画

#

现在该添加动画了!在此最后一步中,您将为登录区域顶部的 LinearProgressIndicator 创建动画。动画具有以下行为:

  • 应用启动时,会在登录区域顶部出现一个很小的红色条。
  • 当一个文本字段包含文本时,红色条会变成橙色,并在登录区域中动画显示 0.15 的距离。
  • 当两个文本字段包含文本时,橙色条会变成黄色,并在登录区域中动画显示一半的距离。
  • 当所有三个文本字段包含文本时,橙色条会变成绿色,并在登录区域中动画显示整个距离。此外, 注册 按钮将启用。
  1. 添加 AnimatedProgressIndicator
    在文件的底部,添加此小部件:

    dart
    class AnimatedProgressIndicator extends StatefulWidget {
      final double value;
    
      const AnimatedProgressIndicator({
        super.key,
        required this.value,
      });
    
      @override
      State<AnimatedProgressIndicator> createState() {
        return _AnimatedProgressIndicatorState();
      }
    }
    
    class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
        with SingleTickerProviderStateMixin {
      late AnimationController _controller;
      late Animation<Color?> _colorAnimation;
      late Animation<double> _curveAnimation;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(
          duration: const Duration(milliseconds: 1200),
          vsync: this,
        );
    
        final colorTween = TweenSequence([
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.red, end: Colors.orange),
            weight: 1,
          ),
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.orange, end: Colors.yellow),
            weight: 1,
          ),
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.yellow, end: Colors.green),
            weight: 1,
          ),
        ]);
    
        _colorAnimation = _controller.drive(colorTween);
        _curveAnimation = _controller.drive(CurveTween(curve: Curves.easeIn));
      }
    
      @override
      void didUpdateWidget(AnimatedProgressIndicator oldWidget) {
        super.didUpdateWidget(oldWidget);
        _controller.animateTo(widget.value);
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: _controller,
          builder: (context, child) => LinearProgressIndicator(
            value: _curveAnimation.value,
            valueColor: _colorAnimation,
            backgroundColor: _colorAnimation.value?.withValues(alpha: 0.4),
          ),
        );
      }
    }

didUpdateWidget 函数会在 AnimatedProgressIndicator 发生更改时更新 AnimatedProgressIndicatorState

  • 使用新的 AnimatedProgressIndicator
    然后,将 Form 中的 LinearProgressIndicator 替换为此新的 AnimatedProgressIndicator

    dart
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        AnimatedProgressIndicator(value: _formProgress), // NEW
        Text('Sign up', style: Theme.of(context).textTheme.headlineMedium),
        Padding(

    此小部件使用 AnimatedBuilder 来将进度指示器动画到最新值。

  • 运行应用。
    在三个字段中输入任何内容以验证动画是否有效,以及单击 注册 按钮是否会调出 欢迎 屏幕。

  • 完整示例

    #
    import 'package:flutter/material.dart';
    
    void main() => runApp(const SignUpApp());
    
    class SignUpApp extends StatelessWidget {
      const SignUpApp({super.key});
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          routes: {
            '/': (context) => const SignUpScreen(),
            '/welcome': (context) => const WelcomeScreen(),
          },
        );
      }
    }
    
    class SignUpScreen extends StatelessWidget {
      const SignUpScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          backgroundColor: Colors.grey[200],
          body: const Center(
            child: SizedBox(
              width: 400,
              child: Card(
                child: SignUpForm(),
              ),
            ),
          ),
        );
      }
    }
    
    class WelcomeScreen extends StatelessWidget {
      const WelcomeScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Text(
              'Welcome!',
              style: Theme.of(context).textTheme.displayMedium,
            ),
          ),
        );
      }
    }
    
    class SignUpForm extends StatefulWidget {
      const SignUpForm({super.key});
    
      @override
      State<SignUpForm> createState() => _SignUpFormState();
    }
    
    class _SignUpFormState extends State<SignUpForm> {
      final _firstNameTextController = TextEditingController();
      final _lastNameTextController = TextEditingController();
      final _usernameTextController = TextEditingController();
    
      double _formProgress = 0;
    
      void _updateFormProgress() {
        var progress = 0.0;
        final controllers = [
          _firstNameTextController,
          _lastNameTextController,
          _usernameTextController
        ];
    
        for (final controller in controllers) {
          if (controller.value.text.isNotEmpty) {
            progress += 1 / controllers.length;
          }
        }
    
        setState(() {
          _formProgress = progress;
        });
      }
    
      void _showWelcomeScreen() {
        Navigator.of(context).pushNamed('/welcome');
      }
    
      @override
      Widget build(BuildContext context) {
        return Form(
          onChanged: _updateFormProgress,
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              AnimatedProgressIndicator(value: _formProgress),
              Text('Sign up', style: Theme.of(context).textTheme.headlineMedium),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _firstNameTextController,
                  decoration: const InputDecoration(hintText: 'First name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _lastNameTextController,
                  decoration: const InputDecoration(hintText: 'Last name'),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(8),
                child: TextFormField(
                  controller: _usernameTextController,
                  decoration: const InputDecoration(hintText: 'Username'),
                ),
              ),
              TextButton(
                style: ButtonStyle(
                  foregroundColor: WidgetStateProperty.resolveWith((states) {
                    return states.contains(WidgetState.disabled)
                        ? null
                        : Colors.white;
                  }),
                  backgroundColor: WidgetStateProperty.resolveWith((states) {
                    return states.contains(WidgetState.disabled)
                        ? null
                        : Colors.blue;
                  }),
                ),
                onPressed: _formProgress == 1 ? _showWelcomeScreen : null,
                child: const Text('Sign up'),
              ),
            ],
          ),
        );
      }
    }
    
    class AnimatedProgressIndicator extends StatefulWidget {
      final double value;
    
      const AnimatedProgressIndicator({
        super.key,
        required this.value,
      });
    
      @override
      State<AnimatedProgressIndicator> createState() {
        return _AnimatedProgressIndicatorState();
      }
    }
    
    class _AnimatedProgressIndicatorState extends State<AnimatedProgressIndicator>
        with SingleTickerProviderStateMixin {
      late AnimationController _controller;
      late Animation<Color?> _colorAnimation;
      late Animation<double> _curveAnimation;
    
      @override
      void initState() {
        super.initState();
        _controller = AnimationController(
          duration: const Duration(milliseconds: 1200),
          vsync: this,
        );
    
        final colorTween = TweenSequence([
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.red, end: Colors.orange),
            weight: 1,
          ),
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.orange, end: Colors.yellow),
            weight: 1,
          ),
          TweenSequenceItem(
            tween: ColorTween(begin: Colors.yellow, end: Colors.green),
            weight: 1,
          ),
        ]);
    
        _colorAnimation = _controller.drive(colorTween);
        _curveAnimation = _controller.drive(CurveTween(curve: Curves.easeIn));
      }
    
      @override
      void didUpdateWidget(AnimatedProgressIndicator oldWidget) {
        super.didUpdateWidget(oldWidget);
        _controller.animateTo(widget.value);
      }
    
      @override
      Widget build(BuildContext context) {
        return AnimatedBuilder(
          animation: _controller,
          builder: (context, child) => LinearProgressIndicator(
            value: _curveAnimation.value,
            valueColor: _colorAnimation,
            backgroundColor: _colorAnimation.value?.withValues(alpha: 0.4),
          ),
        );
      }
    }
    

    观察结果

    #
    • 可以使用 AnimationController 来运行任何动画。
    • Animation 的值发生变化时,AnimatedBuilder 会重建小部件树。
    • 使用 Tween,可以几乎在任何值之间进行插值,在本例中为 Color

    接下来的步骤?

    #

    恭喜!您已经使用 Flutter 创建了您的第一个 Web 应用!

    如果您想继续使用此示例,也许您可以添加表单验证。有关如何执行此操作的建议,请参阅Flutter 食谱中的使用验证构建表单配方。

    有关 Flutter Web 应用、Dart DevTools 或 Flutter 动画的更多信息,请参见以下内容: