在网络上编写您的第一个 Flutter 应用
这是创建您的第一个 Flutter 网络应用的指南。如果您熟悉面向对象编程以及变量、循环和条件等概念,则可以完成本教程。您无需具备 Dart、移动或网络编程的先前经验。
您将构建的内容
您将实现一个简单的网络应用,用于显示登录屏幕。该屏幕包含三个文本字段:名字、姓氏和用户名。当用户填写这些字段时,进度条会在登录区域的顶部进行动画。当所有三个字段都填写完毕时,进度条会以绿色显示在登录区域的整个宽度上,并且注册按钮将变为可用状态。单击注册按钮会导致欢迎屏幕从屏幕底部进行动画。
动画 GIF 显示了本实验室完成后应用的工作方式。
步骤 0:获取入门网络应用
您将从我们为您提供的简单网络应用开始。
-
启用网络开发。
在命令行中,执行以下命令以确保您已正确安装 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: 命令未找到”,则确保您已安装Flutter SDK并且它在您的路径中。
如果仅将应用用于网络,则无需安装 Android 工具链、Android Studio 和 Xcode 工具。如果你以后希望此应用在移动设备上运行,则需要执行其他安装和设置。
-
列出设备。
为确保已安装网络,请列出可用的设备。你应该会看到类似以下内容$ 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 工具。
-
启动应用会显示在以下 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: MaterialStateProperty.resolveWith((states) { return states.contains(MaterialState.disabled) ? null : Colors.white; }), backgroundColor: MaterialStateProperty.resolveWith((states) { return states.contains(MaterialState.disabled) ? null : Colors.blue; }), ), onPressed: null, child: const Text('Sign up'), ), ], ), ); } }
-
运行示例。
点击 运行 按钮以运行示例。请注意,你可以输入文本字段,但 注册 按钮已禁用。 -
复制代码。
点击代码窗格右上角的剪贴板图标,将 Dart 代码复制到剪贴板。 -
创建新的 Flutter 项目。
在 IDE、编辑器或命令行中,创建新的 Flutter 项目,并将其命名为signin_example
。 -
使用剪贴板中的内容替换
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
小部件的以下类定义
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()
方法。这是构建注册按钮的代码部分。注意按钮的定义方式:它是一个带有蓝色背景的TextButton
,白色文本显示为注册,并且在按下时不执行任何操作。 -
更新
onPressed
属性。
将onPressed
属性更改为调用将显示欢迎屏幕的(不存在的)方法。将
onPressed: null
更改为以下内容onPressed: _showWelcomeScreen,
-
添加
_showWelcomeScreen
方法。
修复分析器报告的_showWelcomeScreen
未定义的错误。在build()
方法的正上方,添加以下函数void _showWelcomeScreen() { Navigator.of(context).pushNamed('/welcome'); }
-
添加
/welcome
路由。
创建连接以显示新屏幕。在SignUpApp
的build()
方法中,在'/'
的下方添加以下路由'/welcome': (context) => const WelcomeScreen(),
-
运行应用。
现在应该启用了注册按钮。单击它以调出欢迎屏幕。请注意它如何从底部进行动画。你可以免费获得这种行为。
观察
- 函数
_showWelcomeScreen()
在build()
方法中用作回调函数。回调函数经常在 Dart 代码中使用,在这种情况下,这意味着“在按钮按下时调用此方法”。 - 构造函数前面的
const
关键字非常重要。当 Flutter 遇到常量小部件时,它会在引擎盖下短路大部分重建工作,从而使渲染更有效率。 - Flutter 只有一个
Navigator
对象。此小部件在堆栈中管理 Flutter 的屏幕(也称为路由或页面)。堆栈顶部的屏幕是当前显示的视图。将新屏幕推入此堆栈会将显示切换到该新屏幕。这就是_showWelcomeScreen
函数将WelcomeScreen
推入 Navigator 堆栈的原因。用户单击按钮,欢迎屏幕就会出现。同样,在Navigator
上调用pop()
会返回到前一个屏幕。由于 Flutter 的导航已集成到浏览器的导航中,因此在单击浏览器的后退箭头按钮时会隐式发生这种情况。
步骤 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
小部件的onChanged
参数添加一个回调。添加标记为 NEW 的以下代码return Form( onChanged: _updateFormProgress, // NEW child: Column(
-
更新
onPressed
属性(再次)。
在步骤 1
中,您修改了 注册 按钮的onPressed
属性,以显示欢迎屏幕。现在,更新该按钮,仅当表单完全填写时才显示欢迎屏幕TextButton( style: ButtonStyle( foregroundColor: MaterialStateProperty.resolveWith((states) { return states.contains(MaterialState.disabled) ? null : Colors.white; }), backgroundColor: MaterialStateProperty.resolveWith((states) { return states.contains(MaterialState.disabled) ? null : Colors.blue; }), ), onPressed: _formProgress == 1 ? _showWelcomeScreen : null, // UPDATED child: const Text('Sign up'), ),
-
运行应用。
注册 按钮最初处于禁用状态,但当所有三个文本字段包含(任何)文本时,它将变为启用状态。
观察
- 调用小部件的
setState()
方法会告诉 Flutter,需要在屏幕上更新小部件。然后,框架会处理掉之前的不可变小部件(及其子级),创建一个新的(及其附带的子小部件树),并将其呈现到屏幕上。为了无缝地工作,Flutter 需要快速。必须在不到 1/60 秒的时间内创建新的窗口小部件树并将其呈现到屏幕上,以创建平滑的视觉过渡,特别是对于动画。幸运的是,Flutter 确实 很快。 - 将
progress
字段定义为浮动值,并在_updateFormProgress
方法中更新。当所有三个字段都填写完毕时,_formProgress
设置为 1.0。当_formProgress
设置为 1.0 时,onPressed
回调被设置为_showWelcomeScreen
方法。现在,它的onPressed
参数为非空,按钮已启用。与 Flutter 中的大多数 Material Design 按钮一样,如果 TextButton 的onPressed
和onLongPress
回调为 null,则它们默认处于禁用状态。 -
请注意,
_updateFormProgress
将一个函数传递给setState()
。这称为匿名函数,具有以下语法methodName(() {...});
其中
methodName
是一个命名函数,它将匿名回调函数作为参数。 -
上一步中显示欢迎屏幕的 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,则有一个快捷方式。有关更多信息,请参阅本节末尾的提示。
-
运行应用。
如果您的应用当前未运行,请启动它。从下拉菜单中选择Chrome设备,并从您的 IDE 启动它,或者从命令行使用flutter run -d chrome
, -
获取 DevTools 的 Web 套接字信息。
在命令行或 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。
您是否已安装 DevTools?如果您使用的是 IDE,请确保已按照 VS Code 和 Android Studio 和 IntelliJ 页面中的说明设置好 Flutter 和 Dart 插件。如果您在命令行上工作,请按照 DevTools 命令行 页面中的说明启动 DevTools 服务器。 -
连接到 DevTools。
当 DevTools 启动时,您应该会看到类似以下内容Serving DevTools at http://127.0.0.1:9100
在 Chrome 浏览器中访问此 URL。您应该会看到 DevTools 启动屏幕。它应该如下所示
-
连接到正在运行的应用。
在连接到正在运行的网站下,粘贴您在步骤 2 中复制的 ws 位置,然后单击连接。您现在应该看到 Dart DevTools 在您的 Chrome 浏览器中成功运行恭喜,您现在正在运行 Dart DevTools!
-
设置一个断点。
现在您已经运行了 DevTools,请选择顶部蓝色栏中的调试器选项卡。调试器窗格出现,在左下角,您会看到示例中使用的库列表。选择lib/main.dart
以在中心窗格中显示您的 Dart 代码。 -
设置一个断点。
在 Dart 代码中,向下滚动到更新progress
的位置for (final controller in controllers) { if (controller.value.text.isNotEmpty) { progress += 1 / controllers.length; } }
通过单击行号左侧,在 for 循环所在的行上放置一个断点。断点现在出现在窗口左侧的断点部分。
-
触发断点。
在正在运行的应用中,单击其中一个文本字段以获取焦点。应用会命中断点并暂停。在 DevTools 屏幕中,您可以在左侧看到progress
的值,即 0。这是意料之中的,因为没有填写任何字段。逐步执行 for 循环以查看程序执行。 -
恢复应用。
通过单击 DevTools 窗口中的绿色恢复按钮恢复应用。 -
删除断点。
再次单击断点以删除它,然后恢复应用。
这为您提供了一个小小的示例,展示了使用 DevTools 可以实现的功能,但还有更多功能!有关更多信息,请参阅 DevTools 文档。
步骤 3:为登录进度添加动画
是时候添加动画了!在此最后一步中,您将为登录区域顶部的 LinearProgressIndicator
创建动画。动画具有以下行为
- 当应用启动时,登录区域顶部会出现一个微小的红色条形。
- 当一个文本字段包含文本时,红色条形变为橙色,并在登录区域中移动 0.15 的距离。
- 当两个文本字段包含文本时,橙色条形变为黄色,并在登录区域中移动一半的距离。
- 当所有三个文本字段都包含文本时,橙色条形变为绿色,并在登录区域中移动整个距离。此外,注册按钮变为可用。
-
添加一个
AnimatedProgressIndicator
。
在此文件的底部,添加此小组件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(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), ), ); } }
每当
AnimatedProgressIndicator
发生变化时,didUpdateWidget
函数都会更新AnimatedProgressIndicatorState
。 -
使用新的
AnimatedProgressIndicator
。
然后,用这个新的AnimatedProgressIndicator
替换Form
中的LinearProgressIndicator
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: MaterialStateProperty.resolveWith((states) {
return states.contains(MaterialState.disabled)
? null
: Colors.white;
}),
backgroundColor: MaterialStateProperty.resolveWith((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({
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(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
会重建小组件树。 - 使用
Tween
,您可以在几乎任何值(在本例中为Color
)之间进行插值。
下一步是什么?
恭喜!您已使用 Flutter 创建了第一个 Web 应用程序!
如果您想继续使用此示例,也许可以添加表单验证。有关如何执行此操作的建议,请参阅 Flutter 烹饪书 中的 构建带有验证的表单 配方。
有关 Flutter Web 应用程序、Dart DevTools 或 Flutter 动画的更多信息,请参阅以下内容
- 动画文档
- Dart DevTools
- 隐式动画 codelab
- Web 示例