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 的重建/重新佈局/重繪。