编写你的第一个 Flutter 网页应用 您所在的位置:网站首页 apP开发者 编写你的第一个 Flutter 网页应用

编写你的第一个 Flutter 网页应用

2023-04-04 11:50| 来源: 网络整理| 查看: 265

编写你的第一个 Flutter 网页应用 Contents

第 0 步: 创建初始化 Web 应用

第 1 步:显示欢迎页面

第 2 步:实现输入进度监听

第 2.5 步:启动 Dart 开发者工具

第3步:为输入进度添加动画效果

完整的示例

下一步,我们该做什么?

小提示

这个 codelab 将引导你初步体验 Flutter 网页应用开发。当然你可能更想去尝试 编写你的第一个 Flutter 应用。需要注意的是,在一切工具顺利安装的基础上,本页面上的 codelab 将可以在移动端和桌面端的网页浏览器里运行。

The web app that you'll be building

本教程可以帮助你你完成第一个 Flutter Web 应用,如果你熟悉面对对象、变量、循环以及条件判断等概念,就可以完成本教程,而无需要 Dart、移动开发和 Web 开发经验。

内容概览

你将实现一个只显示登录页面的简单 Web 应用,这个页面包含了三个文本输入框:名字、姓氏和用户名。当用户向输入框输入内容时,在登录区域顶部显示一个进度条动画效果。当用户完成输入时,绿色的进度条将会跟随着充满整个登录区域的顶部,而且 Sign up 按钮状态变成可点击,点击 Sign up 按钮从屏幕下方弹出一个欢迎页面。

右侧的动图展示了完成该教程后程序的运行效果。

你将学到以下内容:

如何使用 Flutter 构建一个原始的 Web 程序。

Flutter 程序的基本结构。

如何实现一个补间 (Tween) 动画。

如何实现一个有状态 (Stateful) widget 。

如何使用断点调试程序。

你将用到:

我们需要下面三个软件来实现该教程:

Flutter SDK

Chrome 浏览器

Text editor 或 IDE

文本编辑器或 IDE

在开发过程中,你需要将你的应用在 Chrome 浏览器中运行,以便使用 Dart DevTools 进行调试。

第 0 步: 创建初始化 Web 应用

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

Enable web development. 启用 Web 开发。

在命令行观察输出内容,你应该可以看到如下类似的内容,说明 Flutter 安装的没问题:

$ flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel master, 3.4.0-19.0.pre.254, on macOS 12.6 21G115 darwin-arm64, locale en) [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 14.0) [✓] Chrome - develop for the web [✓] Android Studio (version 2021.2) [✓] VS Code (version 1.71.1) [✓] Connected device (4 available) [✓] HTTP Host Availability • No issues found!

如果你看到提示是 “flutter: command not found”,那么就需要确保 Flutter SDK 已经正确地安装,并且在环境变量中做好了配置。

如上所示,显示我们缺少 Android 工具、Android Studio 和 Xcode,如果我们只用于 Web 开发,这些都不是必要的。后续如果你想用于移动端开发,你将需要安装配置这些工具。

查询设备列表。 通过查询设备列表来验证已支持 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 浏览器会自动启动并启用 Flutter 开发者工具。

运行程序将在 DartPad 中显示。 {$ begin main.dart $} import 'package:flutter/material.dart'; void main() => runApp(const SignUpApp()); class SignUpApp extends StatelessWidget { const SignUpApp(); @override Widget build(BuildContext context) { return MaterialApp( routes: { '/': (context) => const SignUpScreen(), }, ); } } class SignUpScreen extends StatelessWidget { const SignUpScreen(); @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(); @override State createState() => _SignUpFormState(); } class _SignUpFormState extends State { 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.0), child: TextFormField( controller: _firstNameTextController, decoration: const InputDecoration(hintText: 'First name'), ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( controller: _lastNameTextController, decoration: const InputDecoration(hintText: 'Last name'), ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( controller: _usernameTextController, decoration: const InputDecoration(hintText: 'Username'), ), ), TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( (Set states) { return states.contains(MaterialState.disabled) ? null : Colors.white; }), backgroundColor: MaterialStateProperty.resolveWith( (Set states) { return states.contains(MaterialState.disabled) ? null : Colors.blue; }), ), onPressed: null, child: const Text('Sign up'), ), ], ), ); } } {$ end main.dart $} {$ begin test.dart $} // Avoid warning on "double _formProgress = 0;" //ignore_for_file: prefer_final_fields {$ end test.dart $}

重点提醒

本页面使用了嵌入式 DartPad 来显示和练习示例。如果你看到的是空白页面,请转到 DartPad troubleshooting page

运行代码示例。 点击 Run 按钮来运行示例代码。你就可以在文本框中输入内容,但是 Sign up 按钮是禁用状态的。

复制代码。 点击代码区域右上角的复制图标复制 Dart 代码。

创建一个新的 Flutter 工程。 使用 IDE、编辑器或者命令行,创建一个名称为 signin_example 的新项目,更多内容可以参考文档 Flutter 开发体验初探。

使用上面我们复制的内容替换 lib/main.dart 文件的内容。

观察和分析

完整的示例代码都位于 lib/main.dart 文件中。

如果你了解 Java ,那 Dart 也会给你一种熟悉的感觉。

应用程序的所有的 UI 的都是通过 Dart 构建的。你可以通过文档 声明式 UI 介绍 了解到更多的信息。

应用的 UI 遵循 Material Design 的设计规范,这是一种在任何设备和平台都可以运行的可视化设计语言。而且你也有其他选择,Flutter 也提供了一款 iOS 设计风格的 Cupertino widget 库。当然你也可以创建自己的自定义 widget 库。

在 Flutter 的世界,万物皆 Widget,甚至连应用本身都是 widget。应用的 UI 可以看作为 widget 树。

第 1 步:显示欢迎页面

SignUpForm 类是一个 Stateful widget。这代表着 widget 的存储信息可动态改变,例如用户输入,或者传递的数据。由于 widget 本身是不可变的(一旦创建不可修改),所有 Flutter 的状态信息存储在一种叫 State 的附加类中。在这个代码示例中,所有的编辑将在一个 _SignUpFormState 的私有类中实现。

Fun fact

The Dart compiler enforces privacy for any identifier prefixed with an underscore. For more information, see the Effective Dart Style Guide.

有趣的事

Dart 编译器会将任何带有下划线前缀标识的视为私有。可查阅 Dart 文档 —— 高效 Dart 语言指南:代码风格 获取更多信息。

首先,在 lib/main.dart 文件中,在 SignUpScreen 类后面添加下面 WelcomeScreen widget 的定义类:

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

接下来,你需要创建一个显示方法,然后使用按钮通过方法控制页面的显示。

找到 _SignUpFormState 类的 build() 方法。这部分代码是用来构建注册按钮的。注意,按钮是如何定义:它是一个背景为蓝色, Sign up 文本为白色的 TextButton 按钮,当我们点击它时,并未执行任何操作。

修改按钮的 onPressed 属性。 将按钮的 onPressed 属性改为调用显示欢迎页面的方法(该方法在下一步创建)。

将 onPressed: null 改为以下内容:

onPressed: _showWelcomeScreen,

新增 _showWelcomeScreen 方法。 修复上述代码导致的编译器提示错误: _showWelcomeScreen is not defined. (未定义 _showWelcomeScreen)。在 build() 方法上方添加下面的方法:

void _showWelcomeScreen() { Navigator.of(context).pushNamed('/welcome'); }

添加 /welcome 页面路由。 为新的页面添加跳转路由。在 SignUpApp 类的 build() 方法中,在 '/' 下面添加如下路由:

'/welcome': (context) => const WelcomeScreen(),

运行该应用程序。 Sign up 按钮现在应该可以点击了。单击注册按钮跳转到欢迎页面。注意,欢迎页面显示是有一个从底部弹出的动画。你可以很简单的实现它。

观察和分析

_showWelcomeScreen() 函数被当成回调函数在 build() 方法中被调用。在 Dart 中你会经常使用回调函数,在这里意味着“点击按钮时调用该方法”。

构造函数前面的 const 关键字至关重要,当 Flutter 遇到一个静态 widget 时,它就会缩短引擎下的大部分重建工作,从而提高渲染效率。

Flutter 中仅存在一个 Navigator 对象。这个 widget 用来管理 Flutter 堆栈中的页面(也可以被称为路由 (routes) 或者页面管理器 (pages))。当前显示的页面是堆栈中最上面的页面,通过往堆栈中 push 新的页面来切换新的页面。这也是 _showWelcomeScreen 函数向 Navigator 堆栈中添加 WelcomeScreen 页面的原因。用户点击按钮,然后出现欢迎页面。同样,可以通过调用 Navigator 的 pop() 方法来返回上一个页面。因为 Flutter 的 navigation 已经集成到浏览器的导航中,所以当点击浏览器的返回箭头也会返回到上一个页面。

第 2 步:实现输入进度监听

在这个页面有三个文本框。下一步,我们将实现监听用户输入表单的进度,并且在表单完成后更新应用的 UI 。

提示

这个简单的示例并未对用户的输入进行准确性验证。如果需要,你可以后面自己添加表单验证。

添加一个用于更新进度 _formProgress 属性的方法。在 _SignUpFormState 类,添加一个名为 _updateFormProgress() 的新方法:

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 属性。

表单改变时调用 _updateFormProgress 方法。 在 _SignUpFormState 类的 build() 方法中,为 Form widget 的 onChanged 参数添加回调函数。注意注释为 NEW 的那行新添加的代码:

return Form( onChanged: _updateFormProgress, // NEW child: Column(

再次更改按钮的 onPressed 属性。 还记得我们在第一步中,我们通过修改 onPressed 属性实现了点击 Sign up 按钮跳转到欢迎页面吗?现在,将它改成只有完成表单输入时才可以点击按钮跳转到欢迎页面。

TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( (Set states) { return states.contains(MaterialState.disabled) ? null : Colors.white; }), backgroundColor: MaterialStateProperty.resolveWith( (Set states) { return states.contains(MaterialState.disabled) ? null : Colors.blue; }), ), onPressed: _formProgress == 1 ? _showWelcomeScreen : null, // UPDATED child: const Text('Sign up'), ),

运行应用。 刚打开页面时 Sign up 按钮是禁用状态,当为三个字段输入内容(任意内容)时将会变成可点击状态。

观察和分析

调用 widget 的 setState() 方法通知 Flutter 页面上的 widget 需要重新构建。框架将销毁之前的不可变 widget (上面说过 widget 一旦创建不可更改)(包含它的子级 widget),然后创建一个新的 widget (包含他的子级 widget 树)并将新的 widget 渲染到页面上。为了使应用运行顺畅, Flutter 需要快速的销毁和创建 widget。新创建的 widget 必须在不到 1/60 秒的时间渲染到页面上,才能创建一个流畅的动画效果。幸运的是 Flutter 就是这么这么快。当然如果你愿意的话,也可以使用文本编辑器。

progress 属性定义为浮点值,并在 _updateFormProgress 方法中更新。当三个输入框都被输入后, _formProgress 设置为 1.0 。当 _formProgress 设置为 1.0 后, onPressed 的回调函数将设置为 _showWelcomeScreen 方法。当 onPressed 参数变为非空时按钮将会变成可点击。所有的 TextButton 在 onPressed 和 onLongPress 回调为空时,默认也是无法点击的,与 Flutter 中其他 Material Design 的按钮一致。

请注意, _updateFormProgress 是通过传递一个函数调用 setState() 。这种被称为匿名函数,语法如下所示:

methodName(() {...});

名为 methodName 的函数把匿名回调函数作为参数。

最后一步显示欢迎页面的 Dart 语法如下所示:

_formProgress == 1 ? _showWelcomeScreen : null

Dart 三目运算语法如下: condition ? expression1 : expression2 。如果 _formProgress == 1 是正确的,则会取 : 左侧的值,在这个示例中会取 _showWelcomeScreen 方法。

第 2.5 步:启动 Dart 开发者工具

如何调试 Flutter Web 应用?所有的 Flutter 应用调试方法没有很大的区别。你应该使用 Dart DevTools!(不要和 Chrome 开发者工具搞混淆了)

虽然我们的应用现在没有 bug ,但是我们依然来验证一下。下面的指引讲明了 DevTools 使用的场景,如果你使用的是 IntelliJ 编辑器则会有更好的方式。可以通过查看文档末尾的提示信息获取更多的信息。

运行应用。 如果应用未启动,启动应用。从下拉选项中选择 Chrome 设备然后使用 IDE 启动,或者在命令行中使用 flutter run -d chrome ,

获取开发者工具(DevTools)的 socket 信息。 在命令行或者 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 ws://127.0.0.1:54998/pJqWWxNv92s=

复制粗体显示的调试服务的地址,你可以用这个地址启动 DevTools 。

确认开发工具已被安装。 你是否 已经安装 DevTools 了呢?如果你使用的是编辑器 (IDE) ,先确认已经用 VS Code 和 Android Studio and IntelliJ 文档描述的方式安装 Flutter 和 Dart 插件。如果你使用的是命令行的方式,用 DevTools command line 文档说明的方式启动开发者工具服务(DevTools server)。

连接到 DevTools。 当 DevTools 启动时,你应该会看到如下类似的内容:

Serving DevTools at http://127.0.0.1:9100

在 Chrome 浏览器中打开上面 URL,你应该可以看到 DevTools 运行页面。如下所示:

Screenshot of the DevTools launch screen

连接到运行的应用。 在 Connect to a running site 下面粘贴你在上面第 2 步中复制的 ws 地址,然后点击连接。现在你应该可以看到 Dart DevTools 成功的运行在你的 Chrome 浏览器中,如下所示:

Screenshot of DevTools running screen

恭喜,你已经成功运行 Dart 开发者工具!

提示

这不是启动开发者工具的唯一方式。如果你使用的是 IntelliJ 编辑器,你可以通过 Flutter Inspector -> More Actions -> Open DevTools 的方式启动开发者工具,如下所示:

Screenshot of Flutter inspector with DevTools menu

设置断点。 现在你以前启动了开发者工具,在上面的蓝色工具栏中选择 Debugger 选项。在左下角出现调试面板,可以查看示例中使用的类库。选择 lib/main.dart 将在页面中间显示 Dart 代码。

Screenshot of the DevTools debugger

设置断点。 在 Dart 代码中,向下拉找到被修改的 progress,如下所示:

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

在 for 循环行的行数前面单击设置断点。这个断点将显示在窗口左侧的 Breakpoints 栏中。

触发断点。 在正在运行的应用中,点击任意一个输入框获取焦点。应用会遇到断点并暂停。在开发者工具页面,你可以在左侧看到 progress 的值是 0 。这是正常的,因为你没有输入任何内容,遍历 for 循环观察应用的运行。

恢复应用程序。 在开发者工具窗口点击绿色的 Resume 按钮来恢复应用程序。

删除断点。 再次点击断点来删除断点和恢复程序。

这里只是粗略的介绍开发者工具的使用方式,还有更多没有讲到。请参考 DevTools 文档 学习更多的内容。

第3步:为输入进度添加动画效果

是时候添加动画效果了!在最后一步,我们将在登录区域上方创建一个进度条动画,特效如下所述:

刚启动时,登录区域的顶部显示一条红色的进度条。

当一个文本框被键入内容时,进度条从红色变成橙色,并且进度条前进到距登录区域顶部 1/3 的位置。

当第二个文本框被键入内容时,进度条从橙色变为黄色,并且进度条前进到距登录区域顶部 2/3 的位置。

当三个文本框全部被输入内容时,进度条从橙色变成绿色,并且逐渐充满整个登录区域顶部。除此之外, Sign up 按钮的状态也变成可点击。

添加进度条动画效果 (AnimatedProgressIndicator) 在文件的下面,添加下面的 widget:

class AnimatedProgressIndicator extends StatefulWidget { final double value; const AnimatedProgressIndicator({ required this.value, }); @override State createState() { return _AnimatedProgressIndicatorState(); } } class _AnimatedProgressIndicatorState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _colorAnimation; late Animation _curveAnimation; @override void initState() { super.initState(); _controller = AnimationController( duration: 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(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?.withOpacity(0.4), ), ); } }

didUpdateWidget 方法会在 AnimatedProgressIndicator 变化时更新 AnimatedProgressIndicatorState。

使用新的进度条。 然后,使用新的 AnimatedProgressIndicator widget 替换表单中的 LinearProgressIndicator widget,如下所示:

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

该 widget 使用 AnimatedBuilder 为最新值实现了进度的动画显示。

运行应用。 在三个输入框中输入任意值来验证动画效果是否正常显示,然后点击 Sign up 按钮将弹出欢迎页面。

完整的示例 import 'package:flutter/material.dart'; void main() => runApp(const SignUpApp()); class SignUpApp extends StatelessWidget { const SignUpApp(); @override Widget build(BuildContext context) { return MaterialApp( routes: { '/': (context) => const SignUpScreen(), '/welcome': (context) => const WelcomeScreen(), }, ); } } class SignUpScreen extends StatelessWidget { const SignUpScreen(); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.grey[200], body: Center( child: SizedBox( width: 400, child: Card( child: SignUpForm(), ), ), ), ); } } class WelcomeScreen extends StatelessWidget { const WelcomeScreen(); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text('Welcome!', style: Theme.of(context).textTheme.displayMedium), ), ); } } class SignUpForm extends StatefulWidget { @override State createState() => _SignUpFormState(); } class _SignUpFormState extends State { 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.0), child: TextFormField( controller: _firstNameTextController, decoration: const InputDecoration(hintText: 'First name'), ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( controller: _lastNameTextController, decoration: const InputDecoration(hintText: 'Last name'), ), ), Padding( padding: const EdgeInsets.all(8.0), child: TextFormField( controller: _usernameTextController, decoration: const InputDecoration(hintText: 'Username'), ), ), TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith( (Set states) { return states.contains(MaterialState.disabled) ? null : Colors.white; }), backgroundColor: MaterialStateProperty.resolveWith( (Set states) { return states.contains(MaterialState.disabled) ? null : Colors.blue; }), ), onPressed: _formProgress == 1 ? _showWelcomeScreen : null, child: const Text('Sign up'), ), ], ), ); } } class AnimatedProgressIndicator extends StatefulWidget { final double value; const AnimatedProgressIndicator({ required this.value, }); @override State createState() { return _AnimatedProgressIndicatorState(); } } class _AnimatedProgressIndicatorState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _colorAnimation; late Animation _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(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?.withOpacity(0.4), ), ); } } 观察和分析

你可以使用 AnimationController 控制任何动画效果。

当 Animation 的值改变时 AnimatedBuilder 将重新构建 widget 树。

使用动画 Tween ,你还可以使用很多值,像这个示例中的 Color。

下一步,我们该做什么?

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

如果你想继续完善这个示例,或许你可以添加表单验证。如何继续的建议,请参考 Flutter cookbook 中的 Building a form with validation

有关 Web 应用、Dart 开发者工具以及 Flutter 动画的更多信息,请参考下面文档:

Animation docs Dart DevTools Implicit animations codelab Web samples


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有