Flutter 的热重载功能可帮助您快速轻松地进行实验、构建 UI、添加功能和修复错误。热重载通过将更新后的源文件注入正在运行的 Dart 虚拟机 (VM) 来工作。VM 更新类中的字段和函数的新版本后,Flutter 框架会自动重建 widget 树,使您能够快速查看更改的效果。

Hot reload GIF
DartPad 中热重载的演示

如何执行热重载

#

热重载 Flutter 应用

  1. 从受支持的 Flutter 编辑器或终端窗口运行应用。物理设备或虚拟设备都可以是目标。只有处于调试模式下的 Flutter 应用才能进行热重载或热重启。

  2. 修改项目中的 Dart 文件之一。大多数类型的代码更改都可以进行热重载;有关需要热重启的更改列表,请参阅特殊情况

  3. 如果您使用的是支持 Flutter IDE 工具且启用了保存时热重载的 IDE/编辑器,请选择保存全部 (cmd-s/ctrl-s),或单击工具栏上的热重载按钮。

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

成功进行热重载操作后,您会在控制台中看到类似以下的消息:

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

应用将更新以反映您的更改,并且应用的当前状态将得到保留。您的应用将继续从运行热重载命令之前的状态继续执行。代码已更新,并且执行将继续。

Android Studio UI
Android Studio 中运行、运行调试、热重载和热重启的控件

代码更改只有在修改后的 Dart 代码在更改后再次运行时才可见。具体来说,热重载会使所有现有 widget 重新构建。只有参与 widget 重建的代码才会被自动重新执行。例如,main()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 ')'
    );
    ^

在这种情况下,只需更正指定行 Dart 代码中的错误即可继续使用热重载。

CupertinoTabView 的 builder

#

热重载不会应用对 CupertinoTabViewbuilder 所做的更改。有关更多信息,请参阅Issue 43574

枚举类型

#

当枚举类型更改为普通类或普通类更改为枚举类型时,热重载将不起作用。

例如

更改前

dart
enum Color { red, green, blue }

更改后

dart
class Color {
  Color(this.i, this.j);
  final int i;
  final int j;
}

泛型类型

#

当泛型类型声明被修改时,热重载将不起作用。例如,以下将不起作用:

更改前

dart
class A<T> {
  T? i;
}

更改后

dart
class A<T, V> {
  T? i;
  V? v;
}

原生代码

#

如果您更改了原生代码(例如 Kotlin、Java、Swift 或 Objective-C),则必须执行完全重启(停止并重启应用)才能看到更改生效。

保留先前状态并合并新代码

#

Flutter 的有状态热重载会保留应用的状态。这种方法使您只能查看最近一次更改的效果,而无需丢弃当前状态。例如,如果您的应用需要用户登录,您可以修改并热重载导航层次结构中更深层的页面,而无需重新输入登录凭据。状态会被保留,这通常是期望的行为。

如果代码更改会影响您应用的状态(或其依赖项),则您的应用必须处理的数据可能与从头开始执行时的数据不完全一致。结果可能是热重载后的行为与热重启后的行为不同。

包含最近的代码更改,但排除应用状态

#

在 Dart 中,静态字段是惰性初始化的。这意味着您第一次运行 Flutter 应用并读取静态字段时,它会被设置为其初始化器评估到的任何值。全局变量和静态字段被视为状态,因此在热重载期间不会重新初始化。

如果您更改了全局变量和静态字段的初始化器,则需要进行热重启或重启持有初始化器的状态才能看到更改。例如,考虑以下代码:

dart
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')]),
    ],
  ),
];

运行应用后,您做出以下更改:

dart
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
      ),
    ],
  ),
];

您进行热重载,但更改未得到反映。

相反,在以下示例中:

dart
const foo = 1;
final bar = foo;
void onClick() {
  print(foo);
  print(bar);
}

第一次运行应用会打印 11。然后,您做出以下更改:

dart
const foo = 2; // modified
final bar = foo;
void onClick() {
  print(foo);
  print(bar);
}

虽然对 const 字段值的更改始终会进行热重载,但静态字段初始化器不会重新运行。从概念上讲,const 字段被视为别名而不是状态。

Dart VM 会检测初始化器的更改,并在需要热重启才能生效的更改集上进行标记。尽管上述示例中的大部分初始化工作都会触发标记机制,但像以下情况则不会:

dart
final bar = foo;

要在热重载后更新 foo 并查看更改,请考虑将字段重新定义为 const 或使用 getter 来返回该值,而不是使用 final。例如,以下任一解决方案都可以:

dart
const foo = 1;
const bar = foo; // Convert foo to a const...
void onClick() {
  print(foo);
  print(bar);
}
dart
const foo = 1;
int get bar => foo; // ...or provide a getter.
void onClick() {
  print(foo);
  print(bar);
}

有关更多信息,请阅读 Dart 中 constfinal 关键字的区别:const 和 final 关键字的区别

排除最近的 UI 更改

#

即使热重载操作似乎成功且未引发任何异常,某些代码更改也可能不会在刷新后的 UI 中显示。在更改应用的 main()initState() 方法后,这种行为很常见。

总的来说,如果修改后的代码位于根 widget 的 build() 方法的下游,那么热重载的行为将符合预期。但是,如果修改后的代码不会因重建 widget 树而被重新执行,那么您在热重载后将看不到其效果。

例如,考虑以下代码:

dart
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'));
  }
}

运行此应用后,进行以下更改:

dart
import 'package:flutter/widgets.dart';

void main() {
  runApp(const Center(child: Text('Hello', textDirection: TextDirection.ltr)));
}

通过热重启,程序将从头开始,执行新版本的 main(),并构建一个显示文本 Hello 的 widget 树。

但是,如果您在此更改后热重载应用,main()initState() 不会重新执行,widget 树将使用未更改的 MyApp 实例作为根 widget 进行重建。这导致热重载后没有可见的变化。

工作原理

#

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

  • 任何代码已更改的库
  • 应用的主库
  • 从主库到受影响库的库

这些库的源代码会被编译成 kernel 文件,并发送到移动设备的 Dart VM。

Dart VM 会从新的 kernel 文件重新加载所有库。到目前为止,还没有代码被重新执行。

热重载机制随后会导致 Flutter 框架触发所有现有 widget 和 render object 的重建/重新布局/重绘。