熱重載 (Hot reload)
Flutter 的熱重載功能可幫助你在無需重新啟動應用程式的情況下快速、輕鬆地測試、建構使用者介面、新增功能以及修復錯誤。透過將更新的原始碼檔案注入到正在執行的 Dart 虛擬機器(VM) 來實現熱重載。在虛擬機器使用新的欄位和函式更新類之後, Flutter 框架會自動重新建構 widget 樹,以便你可以快速檢視更改的效果。
如何進行熱重載
想要熱重載 Flutter 應用:
-
在支援 Flutter 編輯器 或終端視窗執行應用程式,物理機或虛擬器都可以。 Flutter 應用程式只有在 DEBUG 模式下才能執行熱重載或者熱重啟。
-
修改專案中的一個 Dart 檔案。大多數型別的程式碼更改可以熱重載,然而一些 特別情況 需要熱重啟應用程式以生效。
-
如果你在支援 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 中的執行、執行除錯、熱重載和熱重啟的控制項位置
只有修改後的 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
熱重載對 CupertinoTabView
的 builder
不起作用。你可以檢視 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);
}
第一次執行應用程式會列印 1
和 1
。然後,如果你進行以下更改:
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 中 const
和 final
關鍵字的區別 瞭解更多。
使用者介面沒有改變
即使熱重載操作看起來成功了並且沒有丟擲例外,但某些程式碼更改可能在更新的 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)。