热重载 (Hot reload)

Flutter 的热重载功能可帮助您在无需重新启动应用程序的情况下快速、轻松地测试、构建用户界面、添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM)来实现热重载。在虚拟机使用新的字段和函数更新类之后,Flutter 框架会自动重新构建 widget 树,以便您可以快速查看更改的效果。

要热重载 Flutter 应用程序:

  • 在支持 Flutter 编辑器 或终端窗口运行应用程序,物理机或虚拟器都可以。Flutter 应用程序只有在调试模式下才能被热重载。

  • 修改项目中的一个Dart文件。大多数类型的代码更改可以热重载;一些需要重新启动应用程序的更改列表,请参阅 限制

  • 如果您在支持 Flutter IDE 工具的 IDE /编辑器中工作,请选择 Save All (cmd-s/ctrl-s),或单击工具栏上的 Hot Reload 按钮。

Hot reload

如果您正在使用命令行 flutter run 运行应用程序,请在终端窗口输入 r

成功执行热重载后,您将在控制台中看到类似于以下内容的消息:

  1. Performing hot reload...
  2. Reloaded 1 of 448 libraries in 978ms.

应用程序更新以反映您的更改,并且应用程序的当前状态(上面示例中的计数器变量的值)将保留。您的应用程序将继续从之前运行热重载命令的位置开始执行。代码被更新并继续执行。

只有修改后的 Dart 代码再次运行时,代码更改才会产生可见效果。具体来说,热重载会导致所有现有的 widgets 重新构建。只有与 widgets 重新构建相关的代码才会自动重新执行。

接下来的部分将介绍修改后的代码在热重载后不会再次运行的常见情况。在某些情况下,对 Dart 代码的小改动将确保您能够继续使用热重载。

编译错误

当代码更改导致编译错误时,热重载会生成类似于以下内容的错误消息:

  1. Hot reload was rejected:
  2. '/Users/obiwan/Library/Developer/CoreSimulator/Devices/AC94F0FF-16F7-46C8-B4BF-218B73C547AC/data/Containers/Data/Application/4F72B076-42AD-44A4-A7CF-57D9F93E895E/tmp/ios_testWIDYdS/ios_test/lib/main.dart': warning: line 16 pos 38: unbalanced '{' opens here
  3. Widget build(BuildContext context) {
  4. ^
  5. '/Users/obiwan/Library/Developer/CoreSimulator/Devices/AC94F0FF-16F7-46C8-B4BF-218B73C547AC/data/Containers/Data/Application/4F72B076-42AD-44A4-A7CF-57D9F93E895E/tmp/ios_testWIDYdS/ios_test/lib/main.dart': error: line 33 pos 5: unbalanced ')'
  6. );
  7. ^

在这种情况下,只需更正上述代码的错误,即可以继续使用热重载。

先前的状态与新代码并存

Flutter 的热重载功能(有时称为有状态热重载)可保留您的应用程序的状态。这种设计允许您能查看最近更改的效果,并且不会丢弃当前状态。例如,如果您的应用需要用户登录,您可以在导航层次结构中下几个级别修改并重新加载页面,而无需重新输入登录凭据。状态保持不变,这通常是期望的行为。

如果代码更改会影响应用程序(或其依赖项)的状态,则应用程序使用的数据可能与它从头开始执行的数据不完全一致。在热重载和完全重启之后,结果可能是不同的行为。

例如,如果您将某个类的定义从 StatelessWidget 改为 StatefulWidget(或反向更改),则在热重载之后,应用程序的以前状态将保留。但是,该状态可能与新的更改不兼容。

参考以下代码:

  1. class MyWidget extends StatelessWidget {
  2. Widget build(BuildContext context) {
  3. return GestureDetector(onTap: () => print('T'));
  4. }
  5. }

运行应用程序后,如果进行以下更改:

  1. class MyWidget extends StatefulWidget {
  2. @override
  3. State<MyWidget> createState() => MyWidgetState();
  4. }
  5. class MyWidgetState extends State<MyWidget> { /*...*/ }

热重载后;控制台将显示类似于以下内容的断言失败的信息:

  1. MyWidget is not a subtype of StatelessWidget

在这些情况下,需要完全重新启动才可以查看更新后的应用程序.

代码发生更改但应用程序的状态没有改变

在 Dart 中,静态字段会被惰性初始化。这意味着第一次运行 Flutter 应用程序并读取静态字段时,会将静态字段的值设为其初始表达式的结果。全局变量和静态字段都被视为状态,因此在热重载期间不会重新初始化。

如果更改全局变量和静态字段的初始化语句,则需要完全重启以查看更改。例如,参考以下代码:

  1. final sampleTable = [
  2. Table("T1"),
  3. Table("T2"),
  4. Table("T3"),
  5. Table("T4"),
  6. ];

运行应用程序后,如果进行以下更改:

  1. final sampleTable = [
  2. Table("T1"),
  3. Table("T2"),
  4. Table("T3"),
  5. Table("T10"), // modified
  6. ];

热重载后,这个改变并没有产生效果。

相反,在下面示例中:

  1. const foo = 1;
  2. final bar = foo;
  3. void onClick() {
  4. print(foo);
  5. print(bar);
  6. }

第一次运行应用程序会打印 11。然后,如果您进行以下更改:

  1. const foo = 2; // modified
  2. final bar = foo;
  3. void onClick() {
  4. print(foo);
  5. print(bar);
  6. }

热重载后,现在打印出 21。虽然对 const 定义的字段值的更改始终会重新加载,但不会重新运行静态字段的初始化语句。从概念上讲,const 字段被视为别名而不是状态。

Dart VM 在一组更改需要完全重启才能生效时,会检测初始化程序更改和标志。在上面的示例中,大部分初始化工作都会触发标记机制,但不适用于以下情况:

  1. final bar = foo;

为了能够更改 foo 并在热重载后查看更改,应该将字段重新用 const 定义或使用 getter 来返回值,而不是使用 final。例如:

  1. const bar = foo;

或者:

  1. get bar => foo;

了解更多 Dart 中关于 const 和 final 关键字的区别.

用户界面没有改变

即使热重载操作看起来成功了并且没有抛出异常,但某些代码更改可能在更新的 UI 中不可见。这种行为在更改应用程序的 main() 方法后很常见。

作为一般规则,如果修改后的代码位于根 widget 的构建方法的下游,则热重载将按预期运行。但是,如果修改后的代码不会因重新构建 widget树而重新执行的话,那么在热重载后您将看不到它的效果。

例如,参考以下代码:

  1. import 'package:flutter/material.dart';
  2. void main() {
  3. runApp(MyApp());
  4. }
  5. class MyApp extends StatelessWidget {
  6. Widget build(BuildContext context) {
  7. return GestureDetector(onTap: () => print('tapped'));
  8. }
  9. }

运行应用程序后,你可能会像如下示例更改代码:

  1. import 'package:flutter/widgets.dart';
  2. void main() {
  3. runApp(const Center(
  4. child: const Text('Hello', textDirection: TextDirection.ltr)));
  5. }

完全重启后,程序会从头开始执行新的 main() 方法,并构建一个 widget 树来显示文本 Hello

但是,如果您在更改后是通过热重载运行,main() 方法则不会重新执行,并且会使用未修改的 MyApp 实例作为根 widget 树来构建新的 widget 树,热重载后结果没有变化。

但是,如果您在此更改后热重新加载应用程序,main()则不会重新执行,并且使用未更改的实例MyApp作为根小部件重建窗口小部件树。热重载后结果没有明显变化。

限制

您可能还会遇到极少数根本不支持热重载的情况。这些包括:

  • 更改 initState() 方法,热重载后不会产生效果,需要重新启动。

  • 枚举类型更改为常规类或常规类更改为枚举类型。例如,如果您更改:

  1. enum Color {
  2. red,
  3. green,
  4. blue
  5. }

改为:

  1. class Color {
  2. Color(this.i, this.j);
  3. final int i;
  4. final int j;
  5. }
  • 泛型类声明被修改。例如,如果您更改:
  1. class A<T> {
  2. T i;
  3. }

改为:

  1. class A<T, V> {
  2. T i;
  3. V v;
  4. }

在这些情况下,热重载会生成诊断消息,并会失败,也不会提交任何改变。

如何实现

调用热重载时,主机会查看自上次编译以来编辑的代码。重新编译以下文件:

  • 任何有代码更改的文件;

  • 应用程序的主入口文件。

  • 受主入口文件影响的文件。

在 Dart 2 中,这些文件的 Dart 源代码被转换为 内核文件并发送到移动设备的 Dart VM。

Dart VM 重新加载新内核文件中的所有文件。到目前为止,没有重新执行代码。

然后,热重载机制使 Flutter 框架触发所有现有的 widgets 和渲染对象的重建/重新布局/重绘。