11 Speaking of Life Cycles, What Are We Talking About

11 Speaking of Life Cycles, What Are We Talking About #

Hello, I’m Chen Hang. Today, I want to share with you the topic of the lifecycle in Flutter.

In the previous article, we started with the common misconception of the “all-purpose” StatefulWidget, and reviewed the UI update mechanism of Widgets together.

With the static configuration passed in by the parent Widget during initialization, StatelessWidget can completely control its static display. However, StatefulWidget needs to rely on a State object to handle user interactions or internal data changes at specific stages, and reflect them on the UI. These specific stages encompass the entire process from loading to unloading of a component, which is called the lifecycle. Similar to iOS’s ViewController and Android’s Activity, Widgets in Flutter also have a lifecycle, and it is represented through States.

App, on the other hand, is a special kind of Widget. In addition to dealing with the various stages of view display (i.e., view lifecycle), it also needs to handle the various states that the application goes through from start to exit (App lifecycle).

For developers, whether it’s a regular Widget (State) or an App, the framework provides us with lifecycle callbacks, allowing us to choose the appropriate timing to do the right things. Therefore, after gaining a deep understanding of the lifecycle, we can write more coherent, smooth, and user-friendly programs.

So today, I will introduce the lifecycles of both Widget (State) and App separately.

State Lifecycle #

The lifecycle of a State refers to the different stages that a Widget associated with the State goes through when the user interacts with it, including creation, display, update, and destruction.

These different stages involve specific task processing. Therefore, it is essential to understand the lifecycle of a State correctly in order to write a well-performing and user-friendly control.

The lifecycle of a State can be divided into three stages, as shown in Figure 1:

Figure 1 State Lifecycle Diagram

As can be seen, the State lifecycle can be divided into three stages: creation (insertion into the widget tree), update (existence in the widget tree), and destruction (removal from the widget tree). Next, let’s take a look at the specific processes of each stage.

Creation #

During initialization, the following methods are executed in order: the constructor method, initState, didChangeDependencies, and build, and then the page rendering is completed.

Let’s take a look at the significance of each method in the initialization process.

  • The constructor method is the starting point of the State lifecycle. Flutter creates a State by calling StatefulWidget.createState(). We can use the constructor method to receive initial UI configuration data passed by the parent Widget. These configuration data determine the initial presentation effect of the Widget.
  • initState is called when the State object is inserted into the widget tree. This function is called only once in the State’s lifecycle. Therefore, we can do some initialization work here, such as setting default values for state variables.
  • didChangeDependencies is used to handle changes in the State object’s dependencies and is called by Flutter after initState() is called.
  • build is responsible for building the view. After the above steps, when the Framework determines that the State is ready, it calls build. In this method, we need to create a Widget based on the initial configuration data passed by the parent Widget and the current state of the State, and then return it.

Update #

Widget state updates are mainly triggered by three methods: setState, didChangeDependencies, and didUpdateWidget.

Next, let’s analyze when these three methods are called.

  • setState: One of the most familiar methods. When the state data changes, we always call this method to tell Flutter, “The data here has changed, please use the updated data to rebuild the UI!”
  • didChangeDependencies: Called by Flutter when the dependencies of the State object change, subsequently triggering component construction. When do the dependencies of the State object change? A typical scenario is when the system language locale or application theme changes, the system notifies the State to execute the didChangeDependencies callback method.
  • didUpdateWidget: Called by the system when the configuration of the Widget changes. For example, when the parent Widget triggers a rebuild (i.e., when the state of the parent Widget changes) or during hot reload.

Once these three methods are called, Flutter will destroy the old Widget and call the build method to reconstruct the Widget.

Destruction #

Component destruction is relatively straightforward. For example, when a component is removed or when a page is destroyed, the system calls the deactivate and dispose methods to remove or destroy the component.

Next, let’s take a look at the specific calling mechanism of these methods:

  • When the visibility state of the component changes, the deactivate function is called, and the State is temporarily removed from the widget tree. It is worth noting that when switching between pages, the position of the State object in the widget tree changes, so it needs to be temporarily removed and then re-added to trigger component construction. Therefore, this function is also called.
  • When the State is permanently removed from the widget tree, the Flutter calls the dispose function. Once at this stage, the component will be destroyed, so we can perform final resource release, remove listeners, clean up the environment, and so on here.

As shown in Figure 2, the left part shows the shared lifecycle between the parent and child Widgets when the state of the parent Widget changes, while the middle and right parts describe how the lifecycle functions of the two associated Widgets respond when switching pages.

Figure 2 State Lifecycle Diagram in Several Common Scenarios

I have prepared a table that summarizes these methods from the dimensions of functionality, invocation timing, and number of invocations to help you understand and memorize them.

Figure 3 Comparison of Method Invocation in the State Lifecycle

In addition, I strongly suggest that you open your IDE, add the above callback functions to the application template, and add print statements to observe the execution order of these functions. This will help you deepen your understanding of the State lifecycle. After all, practice makes perfect.

App Lifecycle #

The lifecycle of a view defines the entire process from loading to building the view, and its callback mechanism ensures that we can do the right things at the right time according to the view’s state. The lifecycle of an app defines the entire process from launching to exiting the app.

In native Android and iOS development, sometimes we need to do some processing in the corresponding app lifecycle events, such as when the app goes from the background to the foreground, from the foreground to the background, or after the UI is drawn.

In native development, we can achieve these requirements by overriding the lifecycle callback methods of Activity or ViewController, or by registering relevant notifications of the application and listening to the app lifecycle to do the corresponding processing. In Flutter, we can use the WidgetsBindingObserver class to achieve the same requirements.

Next, let’s see how to implement such requirements.

First, let’s take a look at the specific callback functions in WidgetsBindingObserver:

abstract class WidgetsBindingObserver {
  // Callback for popping the route
  Future<bool> didPopRoute() => Future<bool>.value(false);
  // Callback for pushing a route
  Future<bool> didPushRoute(String route) => Future<bool>.value(false);
  // Callback for system window changes, such as rotation
  void didChangeMetrics() { }
  // Callback for text scale factor changes
  void didChangeTextScaleFactor() { }
  // Callback for platform brightness changes
  void didChangePlatformBrightness() { }
  // Callback for localization language changes
  void didChangeLocales(List<Locale> locale) { }
  // Callback for app lifecycle changes
  void didChangeAppLifecycleState(AppLifecycleState state) { }
  // Callback for memory pressure
  void didHaveMemoryPressure() { }
  // Callback for accessibility features changes
  void didChangeAccessibilityFeatures() {}
}

As you can see, the WidgetsBindingObserver class provides a rich set of callbacks. Common callbacks such as screen rotation, screen brightness, language changes, and memory warnings can all be implemented using these callbacks. We can listen to the corresponding callback methods by setting a listener for the singleton instance of WidgetsBinding.

Considering that the other callbacks are relatively simple, you can refer to the official documentation and practice with it. Therefore, today I mainly want to share with you the callback didChangeAppLifecycleState for the app lifecycle, as well as the frame rendering callbacks addPostFrameCallback and addPersistentFrameCallback.

Lifecycle Callback #

In the didChangeAppLifecycleState callback function, there is a parameter of type AppLifecycleState, which is an enumeration that encapsulates the app lifecycle states in Flutter. The commonly used states include resumed, inactive, and paused.

  • resumed: The app is visible and can respond to user input.
  • inactive: The app is in an inactive state and cannot handle user responses.
  • paused: The app is not visible and cannot respond to user input, but continues to run in the background.

Here, I would like to share a practical case with you.

In the code below, we register the listener in initState, print the current app state in the didChangeAppLifecycleState callback method, and remove the listener in dispose:

class _MyHomePageState extends State<MyHomePage>  with WidgetsBindingObserver {
    ...
  @override
  @mustCallSuper
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this); // Register the listener
  }
  @override
  @mustCallSuper
  void dispose() {
    super.dispose();
    WidgetsBinding.instance.removeObserver(this); // Remove the listener
  }
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) async {
    print("$state");
    if (state == AppLifecycleState.resumed) {
      // do something
    }
  }
}

Let’s try switching between the foreground and background and observe the app state printed in the console. You will find:

  • When switching from the background to the foreground, the printed app lifecycle changes are: AppLifecycleState.paused -> AppLifecycleState.inactive -> AppLifecycleState.resumed.
  • When switching from the foreground to the background, the printed app lifecycle changes are: AppLifecycleState.resumed -> AppLifecycleState.inactive -> AppLifecycleState.paused.

You can see that the app state printed during the switch between the foreground and background is exactly as expected.

Figure 4 - App lifecycle state changes when switching between foreground and background

Frame Rendering Callback #

In addition to listening to the app lifecycle callbacks and doing the corresponding processing, sometimes we also need to perform operations related to display safety after the component is rendered.

In iOS development, we can use the dispatch_async(dispatch_get_main_queue(),^{...}) method to execute the operation in the next run loop. In Android development, we can use View.post() to insert the message into the message queue to ensure that the operation is performed after the component is rendered.

Actually, implementing the same requirements in Flutter is even simpler: we still use the almighty WidgetsBinding.

WidgetsBinding provides two mechanisms: addPostFrameCallback for a one-time frame rendering callback that will be called after the current frame is rendered and only called once, and addPersistentFrameCallback for a real-time frame rendering callback that will be called after each frame is rendered and can be used for FPS monitoring.

Here is an example:

For a one-time frame rendering callback:

WidgetsBinding.instance.addPostFrameCallback((_){
    print("One-time frame rendering callback"); // will be called only once
});

For a real-time frame rendering callback:

WidgetsBinding.instance.addPersistentFrameCallback((_){
  print("Real-time frame rendering callback"); // will be called for each frame
});

Summary #

In today’s article, I introduced you to the state and app lifecycles, which Flutter provides to us as callbacks to perceive widget and application state changes.

Firstly, I introduced the state as the actual carrier of the widget lifecycle. I divided the state lifecycle into three stages: creation (insertion into the widget tree), update (existence in the widget tree), and destruction (removal from the widget tree). I explained the key methods involved in each stage, hoping that you can deeply understand the complete lifecycle of Flutter components from loading to unloading.

Next, I compared Flutter’s common lifecycle status transition mechanism with the capabilities of the native Android and iOS platforms. I also analyzed the source code of WidgetsBindingObserver. I hope you can master the App lifecycle monitoring methods in Flutter and understand the common lifecycle status transition mechanism in Flutter.

Lastly, we learned about Flutter’s frame drawing callback mechanism and understood the differences and usage scenarios between a one-time frame drawing callback and a real-time frame drawing callback.

In order to accurately control widgets, Flutter provides many state callbacks, so there are quite a few methods mentioned in today’s article. However, as long as you remember the calling rules of the three main threads: creation, update, and destruction, you will be able to string together the calling sequence of these methods, and will be able to perceive state changes and write reasonable components in practical development using the correct methods.

I packaged all the knowledge points covered in today’s sharing into a small project. You can download and run it in your project, and learn it alongside today’s lesson, experiencing the timing of these functions’ calls in different scenarios.

Thought Questions #

Finally, please reflect on these two questions:

  1. The constructor and the initState function are called only once in the lifecycle of a State, and they are mostly used for some initialization work. Based on what we learned today, can you give examples of operations that are suitable to be placed in the constructor, initState, and operations that must be placed in initState?
  2. When Widget reconstruction is triggered by didChangeDependencies, what is the order of lifecycle function calls between parent and child Widgets?

Feel free to leave your thoughts in the comments section, and I will wait for you in the next article! Thank you for reading, and feel free to share this article with more friends.