35 How Is Hot Reload Achieved

35 How is Hot Reload Achieved #

Hi, I’m Chen Hang.

In the previous article, I shared with you the Debug and Release compilation modes in Flutter, as well as how to accurately identify the current compilation mode by using assertions and compile constants, so that you can write code that only works in Debug or Release mode.

Furthermore, Flutter also provides support for using different configuration environments during development and release. We can encapsulate and abstract the configurable parts of the application, use configuration entry points, and inject environment configurations for application startup using InheritedWidget.

If you have experience with native application development, you must know that when developing native applications, if we want to see the adjusted runtime effects on the hardware device, we must go through a long process of recompilation after making code modifications in order to sync them to the device.

However, in the case of Flutter, things are different. Since the Debug mode supports JIT and provides a lot of optimizations for development runtime and debugging, after making code modifications, we can quickly refresh the incremental code through sub-second Hot Reload, without the need for a full code recompilation. This greatly reduces the time required from code modification to seeing the changes take effect.

For example, during the process of developing a page, if we click a button and a popup appears, and we notice that the title of the popup is not aligned properly, we can simply modify the alignment style of the title, save the file, and without the need for a full code recompilation, the title style will change. It feels just like directly modifying the element style in a UI editor panel, which is very convenient.

So, how exactly does Hot Reload work in Flutter?

Hot Reload #

Hot Reload refers to dynamically injecting modified code snippets without interrupting the normal operation of the app. Behind all this is Flutter’s runtime compilation capability. In order to better understand the implementation principle of Hot Reload in Flutter, let’s briefly review the technology behind Flutter’s compilation modes.

  • JIT (Just In Time) is used in Debug mode, which means just-in-time compilation or runtime compilation. It can dynamically distribute and execute code, allowing for faster startup speed. However, the execution performance is affected by runtime compilation.

Figure 1: JIT compilation mode diagram

  • AOT (Ahead Of Time) is used in Release mode, which means ahead-of-time compilation or pre-runtime compilation. It can generate stable binary code for specific platforms, resulting in better execution performance and faster runtime speed. However, each execution requires pre-compilation, which lowers development and debugging efficiency.

Figure 2: AOT compilation mode diagram

As we can see, among the two compilation modes provided by Flutter, AOT is statically compiled, meaning it is compiled into binary code that can be directly executed on the device. On the other hand, JIT is dynamically compiled, meaning that Dart code is compiled into intermediate code (Script Snapshot), which requires Dart VM to interpret and execute at runtime.

The reason why Hot Reload can only be used in Debug mode is because in Debug mode, Flutter uses JIT dynamic compilation, while in Release mode, it uses AOT static compilation. The JIT compiler compiles Dart code into Dart Kernel, which can be dynamically updated, enabling real-time code updates.

Figure 3: Hot Reload process

In general, the Hot Reload process can be divided into five steps: scanning project changes, incremental compilation, pushing updates, code merging, and widget rebuilding:

  1. Project changes: The Hot Reload module will scan each file in the project to check for any additions, deletions, or modifications until it finds Dart code that has changed since the last compilation.

  2. Incremental compilation: The Hot Reload module will compile the changed Dart code into incremental Dart Kernel files.

  3. Pushing updates: The Hot Reload module will send the incremental Dart Kernel files to the running Dart VM on the mobile device through an HTTP port.

  4. Code merging: The Dart VM will merge the received incremental Dart Kernel files with the existing Dart Kernel files, and then reload the new Dart Kernel files.

  5. Widget rebuilding: After confirming that the Dart VM resources have been loaded successfully, Flutter will reset its UI thread and notify the Flutter Framework to rebuild the widget.

As we can see, the Hot Reload provided by Flutter does not restart the app after receiving code changes. Instead, it triggers the widget tree to be redrawn, thus preserving the state before the changes. This greatly saves time when debugging complex interactive interfaces.

For example, if we need to adjust the UI style for a page with a deep view stack, using the traditional recompilation method would not only require a long full compilation time, but also repetitive interactions to restore the view stack in order to re-enter the page and see the changes. However, with Hot Reload, there is no compilation time and the view stack state is preserved. After completing the Hot Reload, we can immediately preview the UI effects, which is equivalent to refreshing a specific part of the interface.

Scenarios that do not support hot reload #

The sub-second hot reload provided by Flutter has always been a powerful debugging tool for developers. With hot reload, we can quickly modify the UI, fix bugs, and see the changes without restarting the application, greatly improving the efficiency of UI debugging.

However, hot reload in Flutter does have some limitations. Because it involves state preservation and restoration, not all code changes can be updated through hot reload.

Next, let me introduce several typical scenarios that do not support hot reload:

  • Code compilation errors;
  • Incompatible widget states;
  • Changes to global variables and static properties;
  • Changes in the main method;
  • Changes in the initState method;
  • Changes to enumerations and generic types.

Now let’s take a closer look at the issues in these scenarios and how to solve them.

Compilation Error in Code #

When code changes result in compilation errors, hot reload will display error messages. For example, in the following example, a closing parenthesis is missing in the code. When using hot reload, the compiler will directly report the error:

Initializing hot reload...
Syncing files to device iPhone X...

Compiler message:
lib/main.dart:84:23: Error: Can't find ')' to match '('.
    return MaterialApp(
                      ^
Reloaded 1 of 462 libraries in 301ms.

In this case, it is only necessary to correct the error in the above code to continue using hot reload.

Incompatible Widget State #

When code changes affect the state of a Widget, it results in inconsistent data between the Widget before and after hot reload. This means that the state preserved by the application is not compatible with the new changes. In such cases, hot reload cannot be used.

For example, in the code below, if we change the definition of a class from StatelessWidget to StatefulWidget, hot reload will fail with an error:

// Before change
class MyWidget extends StatelessWidget {
  Widget build(BuildContext context) {
    return GestureDetector(onTap: () => print('T'));
  }
}

// After change
class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => MyWidgetState();
}
class MyWidgetState extends State<MyWidget> { /*...*/ }

In these situations, the application needs to be restarted in order to see the updated program.

Modifying Global Variables and Static Properties #

In Flutter, global variables and static properties are both considered as states. When the application is first run, their values are set to the result of the initialization statement. Therefore, they are not re-initialized during hot reload.

For example, in the code below, we modify an initialized element of a static Text array. Although hot reload does not throw an error, since static variables are not re-initialized after hot reload, this change will not take effect:

// Before modification
final sampleText = [
  Text("T1"),
  Text("T2"),
  Text("T3"),
  Text("T4"),
];

// After modification
final sampleText = [
  Text("T1"),
  Text("T2"),
  Text("T3"),
  Text("T10"),    // point of modification
];

If we need to modify the initialization statement of global variables and static properties, the application must be restarted to see the changes take effect.

Changes in main method #

In Flutter, any changes made to the main function will not be executed again after hot reload, as only the widget tree rooted at the original root node is recreated. Therefore, if we modify the code within the main function, we cannot see the updated effect through hot reload.

In the first article “预习篇 · 从零开始搭建Flutter开发环境” (Preview: Setting Up Flutter Development Environment from Scratch), I introduced this situation to you. Before the update, we wrapped a text widget displaying “Hello World” in the MyApp class. After the update, we directly wrapped a text widget displaying “Hello 2019” in the main function:

// Before the update
class MyAPP extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const Center(child: Text('Hello World', textDirection: TextDirection.ltr));
  }
}

void main() => runApp(new MyAPP());

// After the update
void main() => runApp(const Center(child: Text('Hello, 2019', textDirection: TextDirection.ltr)));

Since the main function is not executed again after hot reload, the above changes cannot be viewed through hot reload.

Changes in the initState Method #

When hot-reloading, Flutter saves the state of the widget and then rebuilds it. The initState method is where the widget’s state is initialized, and any changes made in this method will conflict with the saved state, so they won’t take effect after hot-reloading.

In the example below, we are changing the initial value of the counter from 10 to 100:

// Before the change
class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  @override
  void initState() {
    _counter = 10;
    super.initState();
  }
  ...
}

// After the change
class _MyHomePageState extends State<MyHomePage> {
  int _counter;
  @override
  void initState() {
    _counter = 100;
    super.initState();
  }
  ...
}

Since such changes occur within the initState method, you won’t be able to see the updated result through hot-reloading. You will need to restart the app to see the changes take effect.

Changes to Enums and Generic Types #

In Flutter, enums and generic types are treated as states, so modifications to them are not supported by hot reload. For example, in the code snippet below, we changed an enum type to a regular class and added a generic parameter to it:

// Before
enum Color {
  red,
  green,
  blue
}

class C<U> {
  U u;
}

// After
class Color {
  Color(this.r, this.g, this.b);
  final int r;
  final int g;
  final int b;
}

class C<U, V> {
  U u;
  V v;
}

Both of these changes will result in a failed hot reload and corresponding error messages. Similarly, we need to restart the application to see the changes take effect.

Summary #

Alright, that’s all for today’s sharing. Let’s summarize the main points we talked about today.

Flutter’s hot reload is based on the JIT compilation mode, which enables code incremental synchronization. Since JIT is a dynamic compilation, it can compile Dart code into intermediate code that can be interpreted and executed by the Dart VM at runtime. Therefore, incremental synchronization can be achieved by dynamically updating the intermediate code.

The process of hot reload can be divided into 5 steps, including: scanning project changes, incremental compilation, pushing updates, merging code, and rebuilding widgets. When Flutter receives code changes, it does not restart the app completely, but triggers a re-rendering of the widget tree. As a result, the previous state is preserved, greatly reducing the time required from code modification to seeing the changes.

On the other hand, due to the need for state preservation and restoration, hot reload is not supported in scenarios involving incompatible states and state initialization. For example, changes that are not compatible with widget states before and after modification, changes to global variables and static properties, changes in the “main” method, changes in the “initState” method, and changes in enums and generics.

It can be seen that hot reload improves the efficiency of debugging UI and is very suitable for scenarios where you need to repeatedly check the modified effects, such as writing interface styles. However, due to the limitations of its state preservation mechanism, hot reload itself has some boundaries that it cannot support.

If you accidentally encounter a scenario that hot reload cannot support when writing business logic, you don’t need to go through the lengthy process of recompiling and loading. Just click the “hot restart” button located in the lower left corner of the project panel, and you can recompile and restart the program in seconds. It is also very fast.

Thought-provoking question #

Finally, I would like to leave you with a thought-provoking question.

Do you understand the hot reload mechanism of other frameworks like React Native and Webpack? How does their hot reload mechanism differ from Flutter’s?

Feel free to leave a comment in the comment section to share your thoughts. I’ll be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends to read together.