热重载 (Hot reload)

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

如何进行热重载

想要热重载 Flutter 应用:

  1. 在支持 Flutter 编辑器 或终端窗口运行应用程序,物理机或虚拟器都可以。 Flutter 应用程序只有在 DEBUG 模式下才能执行热重载或者热重启。

  2. 修改项目中的一个 Dart 文件。大多数类型的代码更改可以热重载,然而一些 特别情况 需要热重启应用程序以生效。

  3. 如果你在支持 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 UI

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

热重载对 CupertinoTabViewbuilder 不起作用。你可以查看 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);
}

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

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 bar = foo;

或者:

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 中 constfinal 关键字的区别 了解更多。

用户界面没有改变

即使热重载操作看起来成功了并且没有抛出异常,但某些代码更改可能在更新的 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)。