编写你的第一个 Flutter 网页应用
本教程可以帮助你你完成第一个 Flutter Web 应用,如果你熟悉面对对象、变量、循环以及条件判断等概念,就可以完成本教程,而无需要 Dart、移动开发和 Web 开发经验。
内容概览
#你将实现一个只显示登录页面的简单 Web 应用,这个页面包含了三个文本输入框:名字、姓氏和用户名。当用户向输入框输入内容时,在登录区域顶部显示一个进度条动画效果。当用户完成输入时,绿色的进度条将会跟随着充满整个登录区域的顶部,而且 Sign up 按钮状态变成可点击,点击 Sign up 按钮从屏幕下方弹出一个欢迎页面。
右侧的动图展示了完成该教程后程序的运行效果。
第 0 步: 创建初始化 Web 应用
#你将从我们为你提供的简单 Web 应用开始学习。
- Enable web development.
启用 Web 开发。
在命令行观察输出内容,你应该可以看到如下类似的内容,说明 Flutter 安装的没问题:
flutter doctor Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.24.5, 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 已经正确地安装,并且在环境变量中做好了配置。
如上所示,显示我们缺少 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 中显示。
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'), ), ], ), ); } }
-
运行代码示例。
点击 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
的私有类中实现。
首先,在 lib/main.dart
文件中,在 SignUpScreen
类后面添加下面 WelcomeScreen
widget 的定义类:
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,
),
),
);
}
}
接下来,你需要创建一个显示方法,然后使用按钮通过方法控制页面的显示。
-
找到
_SignUpFormState
类的build()
方法。这部分代码是用来构建注册按钮的。注意,按钮是如何定义:它是一个背景为蓝色, Sign up 文本为白色的TextButton
按钮,当我们点击它时,并未执行任何操作。 -
修改按钮的
onPressed
属性。
将按钮的onPressed
属性改为调用显示欢迎页面的方法(该方法在下一步创建)。将
onPressed: null
改为以下内容:dartonPressed: _showWelcomeScreen,
-
新增
_showWelcomeScreen
方法。
修复上述代码导致的编译器提示错误:_showWelcomeScreen
is not defined. (未定义_showWelcomeScreen
)。在build()
方法上方添加下面的方法:dartvoid _showWelcomeScreen() { Navigator.of(context).pushNamed('/welcome'); }
-
添加
/welcome
页面路由。
为新的页面添加跳转路由。在SignUpApp
类的build()
方法中,在'/'
下面添加如下路由:dart'/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()
的新方法:dartvoid _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 的那行新添加的代码:dartreturn Form( onChanged: _updateFormProgress, // NEW child: Column(
-
再次更改按钮的
onPressed
属性。
还记得我们在第一步中,我们通过修改onPressed
属性实现了点击 Sign up 按钮跳转到欢迎页面吗?现在,将它改成只有完成表单输入时才可以点击按钮跳转到欢迎页面。dartTextButton( 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'), ),
-
运行应用。
刚打开页面时 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()
。这种被称为匿名函数,语法如下所示:dartmethodName(() {...});
名为
methodName
的函数把匿名回调函数作为参数。 -
最后一步显示欢迎页面的 Dart 语法如下所示:
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 <b>ws://127.0.0.1:54998/pJqWWxNv92s=</b>
复制粗体显示的调试服务的地址,你可以用这个地址启动 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 运行页面。如下所示:
-
连接到运行的应用。
在 Connect to a running site 下面粘贴你在上面第 2 步中复制的 Web Socket (ws) 地址,然后点击 Connect。现在你应该可以看到 Dart DevTools 成功地运行在你的 Chrome 浏览器中,如下所示:恭喜,你已经成功运行 Dart 开发者工具!
-
设置断点。
现在你以前启动了开发者工具,在上面的蓝色工具栏中选择 Debugger 选项。在左下角出现调试面板,可以查看示例中使用的类库。选择lib/main.dart
将在页面中间显示 Dart 代码。 -
设置断点。
在 Dart 代码中,向下拉找到被修改的progress
,如下所示:dartfor (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:dartclass 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?.withOpacity(0.4), ), ); } }
didUpdateWidget
方法会在AnimatedProgressIndicator
变化时更新AnimatedProgressIndicatorState
。 -
使用新的进度条。
然后,使用新的AnimatedProgressIndicator
widget 替换表单中的LinearProgressIndicator
widget,如下所示:dartchild: 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({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?.withOpacity(0.4),
),
);
}
}
观察和分析
#-
你可以使用
AnimationController
控制任何动画效果。 -
当
Animation
的值改变时AnimatedBuilder
将重新构建 widget 树。 -
使用动画
Tween
,你还可以使用很多值,像这个示例中的Color
。
下一步,我们该做什么?
#恭喜!你已经使用 Flutter 创建了第一个 Web 应用!
如果你想继续完善这个示例,或许你可以添加表单验证。如何继续的建议,请参考 Flutter cookbook 中的 Building a form with validation
有关 Web 应用、Dart 开发者工具以及 Flutter 动画的更多信息,请参考下面文档:
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-11-02。 查看文档源码 或者 为本页面内容提出建议。