跳到主內容

熱過載

使用 Flutter 的熱過載功能加速開發。

Flutter 的熱過載功能可幫助您快速輕鬆地進行實驗、構建 UI、新增功能和修復錯誤。熱過載的工作原理是將更新後的原始碼檔案注入到 Dart 虛擬機器(Dart runtime)中。在 Dart 虛擬機器使用新版本的欄位和函式更新類之後,Flutter 框架會自動重建元件樹,從而讓您能夠快速檢視更改的效果。

Hot reload GIF
DartPad 中的熱過載演示

如何執行熱過載

#

要熱過載 Flutter 應用

  1. 從受支援的 Flutter 編輯器或終端視窗執行應用。目標可以是物理裝置或虛擬裝置。只有處於除錯模式(debug mode)的 Flutter 應用才能進行熱過載或熱重啟。

  2. 修改專案中的其中一個 Dart 檔案。大多數型別的程式碼更改都可以進行熱過載;有關需要熱重啟的更改列表,請參閱特殊情況

  3. 如果您使用的 IDE/編輯器支援 Flutter 的 IDE 工具並且啟用了“儲存時熱過載”(hot reload on save),請選擇 全部儲存 (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 的構建器

#

熱過載不會應用對 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 虛擬機器能夠檢測到初始化器的更改,並在需要熱重啟才能生效時進行標記。上述示例中的大多數初始化工作都會觸發該標記機制,但以下情況除外

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 關鍵字的區別

排除了最近的 UI 更改

#

即使熱過載操作看起來成功且沒有生成異常,某些程式碼更改也可能在重新整理後的 UI 中不可見。這種行為在更改應用的 main()initState() 方法後很常見。

通常情況下,如果修改後的程式碼位於根元件的 build() 方法之下,則熱過載會按預期工作。但是,如果修改後的程式碼不會因為元件樹的重建而重新執行,那麼您在熱過載後將看不到其效果。

例如,考慮以下程式碼

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 的元件樹。

但是,如果您在此更改後熱過載應用,則不會重新執行 main()initState(),元件樹將使用未更改的 MyApp 例項作為根元件進行重建。這導致熱過載後沒有可見的變化。

工作原理

#

當呼叫熱過載時,宿主機(host machine)會檢視自上次編譯以來編輯的程式碼。以下庫將被重新編譯

  • 任何包含已更改程式碼的庫
  • 應用的主庫
  • 從主庫到受影響庫路徑上的所有庫

這些庫中的原始碼被編譯成 kernel 檔案併發送到移動裝置的 Dart 虛擬機器。

Dart 虛擬機器從新的 kernel 檔案中重新載入所有庫。到目前為止,還沒有程式碼被重新執行。

熱過載機制隨後觸發 Flutter 框架對所有現有元件和渲染物件進行重建/重新佈局/重繪。