热重载

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

如何执行热重载

要热重载 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 代码时,代码更改才具有可见效果。具体来说,热重载会导致所有现有小部件重建。只有涉及小部件重建的代码才会自动重新执行。例如,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 所做的更改。有关更多信息,请参阅 问题 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 的有状态热重载保留您应用的状态。此方法使您能够仅查看最近更改的效果,而无需丢弃当前状态。例如,如果您的应用要求用户登录,则您可以修改和热重载导航层次结构中低几个级别的页面,而无需重新输入您的登录凭据。保留状态,这通常是所需的行为。

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

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

在 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 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 更改被排除在外

即使热重载操作看似成功并且未生成任何异常,某些代码更改在刷新的 UI 中可能仍不可见。在对应用的 main()initState() 方法进行更改后,这种行为很常见。

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

例如,考虑以下代码

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() 的新版本,并构建一个显示文本 Hello 的小部件树。

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

工作原理

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

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

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

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

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