熱重載 (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)。