热重载 (Hot reload)
Flutter 的热重载功能可帮助你在无需重新启动应用程序的情况下快速、轻松地测试、构建用户界面、添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM) 来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便你可以快速查看更改的效果。
如何进行热重载
#想要热重载 Flutter 应用:
-
在支持 Flutter 编辑器 或终端窗口运行应用程序,物理机或虚拟器都可以。 Flutter 应用程序只有在 DEBUG 模式下才能执行热重载或者热重启。
-
修改项目中的一个 Dart 文件。大多数类型的代码更改可以热重载,然而一些 特别情况 需要热重启应用程序以生效。
-
如果你在支持 Flutter 的 IDE 或编辑器中工作,请选择 Save All (
Command + S
/Ctrl + S
),或单击工具栏上的 Hot Reload 按钮。如果你正在使用命令行
flutter run
运行应用程序,请在终端窗口输入r
。
成功执行热重载后,你将在控制台中看到类似于以下内容的消息:
Performing hot reload...
Reloaded 1 of 448 libraries in 978ms.
应用程序将以你的更改进行更新,并保留应用程序当前的状态。你的应用程序将继续从之前运行热重载命令的位置开始执行。代码被更新并继续执行。
Android Studio 中的运行、运行调试、热重载和热重启的控件位置
只有修改后的 Dart 代码再次运行时,代码更改才会产生可见效果。具体来说,热重载会导致所有现有的 widgets 重新构建。只有与 widgets 重新构建相关的代码才会自动重新执行。
main()
and initState()
方法则不会再次运行。
特别情况
#下面的部分会描述一些热重载的特别的情况。在某些情况下,对 Dart 代码的小改动将确保你能够继续使用热重载。在其他情况下,需要热重启或完全重启。
应用被强制停止
#热重载会在应用被强制停止之后断开连接。比如一直在后台运行的应用(会被系统强制停止)。
编译错误
#当代码更改导致编译错误时,热重载会生成类似于以下内容的错误消息:
Hot reload was rejected:
'/path/to/project/lib/main.dart': warning: line 16 pos 38: unbalanced '{' opens here
Widget build(BuildContext context) {
^
'/path/to/project/lib/main.dart': error: line 33 pos 5: unbalanced ')'
);
^
在这种情况下,只需更正上述代码的错误,即可以继续使用热重载。
CupertinoTabView's builder
#热重载对 CupertinoTabView
的 builder
不起作用。你可以查看 Issue 43574 了解更多细节。
枚举类型
#在枚举类型与普通的类定义互相转换时,热重载无法生效。
例如:
更改前:
enum Color {
red,
green,
blue,
}
更改后:
class Color {
Color(this.i, this.j);
final int i;
final int j;
}
泛型
#在泛型发生改变时,热重载不会生效。下面的例子将不会有效果:
更改前:
class A<T> {
T? i;
}
更改后:
class A<T, V> {
T? i;
V? v;
}
原生代码
#如果你更改了原生代码(例如 Kotlin、Java、Swift 或 Objective-C),你必须要进行完全重启(停止后重新运行应用)才能让更改生效。
新的代码与旧的状态结合
#Flutter 有状态的热重载将保持你的应用的状态。这项特性让你能够在不丢失状态的情况下,预览代码作出的改动。例如,如果你的应用需要用户登录,你可以调整路由相关的内容重载几次,而不需要重新进入登录流程。过程中状态是保持的,一般与预期相符。
如果代码改动会影响你的应用的状态(或应用的依赖),则应用里正在使用的数据可能与从一开始执行的数据不完全一致。热重载和热重启的结果可能不一致。
Recent code change is included but app state is excluded
#在 Dart 中,静态字段是延迟初始化的。这意味着第一次运行 Flutter 应用程序并读取静态字段时,会将静态字段的值设为其初始表达式的结果。全局变量和静态字段都被视为状态,因此在热重载期间不会重新初始化。
如果你改变了全局变量或静态字段的初始化内容,你需要重新
如果更改全局变量和静态字段的初始化语句,则需要完全重启以查看更改。例如,参考以下代码:
final sampleTable = [
Table(
children: const [
TableRow(
children: [Text('T1')],
)
],
),
Table(
children: const [
TableRow(
children: [Text('T2')],
)
],
),
Table(
children: const [
TableRow(
children: [Text('T3')],
)
],
),
Table(
children: const [
TableRow(
children: [Text('T4')],
)
],
),
];
运行应用程序后,如果进行以下更改:
final sampleTable = [
Table(
children: const [
TableRow(
children: [Text('T1')],
)
],
),
Table(
children: const [
TableRow(
children: [Text('T2')],
)
],
),
Table(
children: const [
TableRow(
children: [Text('T3')],
)
],
),
Table(
children: const [
TableRow(
children: [Text('T10')], // modified
)
],
),
];
热重载后,这个改变并没有产生效果。
相反,在下面示例中:
const foo = 1;
final bar = foo;
void onClick() {
print(foo);
print(bar);
}
第一次运行应用程序会打印 1
和 1
。然后,如果你进行以下更改:
const foo = 2; // modified
final bar = foo;
void onClick() {
print(foo);
print(bar);
}
虽然对 const
定义的字段值的更改始终会重新加载,但不会重新运行静态字段的初始化语句。从概念上讲,const
字段被视为别名而不是状态。
Dart VM 在一组更改需要完全重启才能生效时,会检测初始化程序更改和标志。在上面的示例中,大部分初始化工作都会触发标记机制,但不适用于以下情况:
final bar = foo;
为了能够更改 foo
并在热重载后查看更改,应该将字段重新用 const
定义或使用 getter 来返回值,而不是使用 final
。下面的解决方案均可使用:
const foo = 1;
const bar = foo; // Convert foo to a const...
void onClick() {
print(foo);
print(bar);
}
const foo = 1;
int get bar => foo; // ...or provide a getter.
void onClick() {
print(foo);
print(bar);
}
你可以阅读 Dart 中 const
和 final
关键字的区别 了解更多。
用户界面没有改变
#即使热重载操作看起来成功了并且没有抛出异常,但某些代码更改可能在更新的 UI 中不可见。这种行为在更改应用程序的 main()
方法后很常见。
作为一般规则,如果修改后的代码位于根 widget 的
build()
方法的下游,则热重载将按预期运行。但是,如果修改后的代码不会因重新构建 widget 树而重新执行的话,那么在热重载后你将看不到它更改后的效果。
例如,参考以下代码:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return GestureDetector(onTap: () => print('tapped'));
}
}
运行应用程序后,你可能会像如下示例更改代码:
import 'package:flutter/widgets.dart';
void main() {
runApp(const Center(child: Text('Hello', textDirection: TextDirection.ltr)));
}
如果你进行了完全重启,程序会从头开始执行新的 main()
方法,并构建一个 widget 树来显示文本 Hello
。
但是,如果你在更改后是通过热重载运行,
main()
和 initState()
方法不会重新执行,并且会使用未修改的 MyApp
实例作为根 widget 树来构建新的 widget 树,热重载后结果没有变化。
热重载的原理
#调用热重载时,主机会查看自上次编译以来编辑的代码。重新编译以下文件:
-
任何有代码更改的文件
-
应用程序的主入口文件
-
受主入口文件影响的文件
这些库中的源代码被编译为 内核文件,并发送到移动设备的 Dart VM 中。
Dart VM 重新加载新内核文件中的所有文件。到这一步为止,没有重新执行任何代码。
最后,热重载机制在 Flutter 框架中触发所有现有的 widget 和渲染对象的重建/重新布局/重绘 (reassemble)。
除非另有说明,本文档之所提及适用于 Flutter 的最新稳定版本,本页面最后更新时间: 2024-10-09。 查看文档源码 或者 为本页面内容提出建议。