熱重載

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 的建構函式

熱重載不會套用對 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 執行個體作為根小工具重新建立。這會導致熱重載後沒有任何可見的變更。

運作方式

呼叫熱重載時,主機電腦會查看自上次編譯以來已編輯的程式碼。下列函式庫會重新編譯

  • 任何程式碼已變更的函式庫
  • 應用程式的 main 函式庫
  • 從 main 函式庫導致受影響函式庫的函式庫

這些函式庫的原始程式碼編譯成 kernel 檔案,並傳送至行動裝置的 Dart VM。

Dart VM 從新的 kernel 檔案重新載入所有函式庫。目前為止,尚未重新執行任何程式碼。

然後,熱更新機制會導致 Flutter 架構觸發所有現有小工具和渲染物件的重新建構/重新配置/重新繪製。