10 What Exactly Is the State in a Widget

10 What Exactly Is the State in a Widget #

Hello, I’m Chen Hang.

In the previous article, we have gained a deep understanding of how Widget is the cornerstone of building interfaces in Flutter, and how Widget, Element, and RenderObject work together to achieve graphic rendering. Flutter has done a lot of rendering optimization work at the lower level, allowing us to build interfaces of any functionality and complexity by simply combining and nesting different types of Widgets.

At the same time, through our previous studies, we have learned that there are two types of Widgets: StatelessWidget and StatefulWidget. StatefulWidget is used in scenarios where there is interaction and dynamic visual effects are required, while StatelessWidget is used to handle static, stateless view displays. StatefulWidget covers all the scenarios where StatelessWidget is used, so when building interfaces, we often use StatefulWidget extensively to handle static view display requirements, and it seems that there is no problem.

So, what is the necessity of StatelessWidget? Is StatefulWidget a universal tool in Flutter? In today’s article, I will focus on explaining the differences between these two types, so that you can better understand Widgets and grasp the correct usage of different types of Widgets.

UI Programming Paradigm #

To understand the scenarios in which to use StatelessWidget and StatefulWidget, we first need to understand the UI programming paradigm in Flutter.

If you have experience with native systems (Android, iOS) or native JavaScript development, you should know that view development is imperative, meaning you need to precisely tell the operating system or browser how to do things. For example, if we want to change the text of a specific text control on the interface, we need to find the specific text control and call its control method command to complete the text change.

The following code demonstrates how to change the text displayed in a text control to “Hello World” in Android, iOS, and native JavaScript:

// Change the text displayed in a text control to "Hello World" in Android
TextView textView = (TextView) findViewById(R.id.txt);
textView.setText("Hello World");

// Change the text displayed in a text control to "Hello World" in iOS
UILabel *label = (UILabel *)[self.view viewWithTag:1234];
label.text = @"Hello World";

// Change the text displayed in a text control to "Hello World" in native JavaScript
document.querySelector("#demo").innerHTML = "Hello World!";

In contrast, Flutter’s view development is declarative, and its core design concept is to separate views from data, which is completely consistent with the design philosophy of React.

For us, if we want to achieve the same requirement, it would be a bit more complicated: in addition to designing a Widget layout scheme, we also need to maintain a set of text data in advance and bind the data in the dataset to the Widgets that need to change, so that they can be rendered by the Widgets based on this dataset.

However, when we need to change the text on the interface, we only need to change the text data in the dataset and notify the Flutter framework to trigger the Widgets to be re-rendered. In this way, developers no longer need to pay attention to the details of the various processes in UI programming; they just need to maintain the dataset. Compared to the imperative view development approach, which requires setting visual properties for different components (Widgets) one by one, this declarative approach is much more convenient.

In summary, imperative programming emphasizes precise control of process details, while declarative programming emphasizes the overall output of results through intention. In Flutter, the intention is the State that is bound to the component, and the result is the component after it is re-rendered. During the lifecycle of a Widget, any changes applied to the State will force the Widget to be rebuilt.

In scenarios where a component does not need to be changed after it is created, binding the state is optional. This distinction between “optional” determines the two types of Widgets in Flutter: StatelessWidget without bound state, and StatefulWidget with bound state. When you are building a user interface that does not change with any state information, you should choose StatelessWidget; otherwise, choose StatefulWidget. The former is generally used for displaying static content, while the latter is used for presenting content with interactive feedback.

Next, I will introduce StatelessWidget and StatefulWidget separately, analyze their differences from the source code, and summarize some basic principles for choosing Widgets.

StatelessWidget #

In Flutter, widgets are built in a top-down manner, from parent to child. The parent widget controls the display style of the child widget, and the style configuration is provided by the parent widget during construction.

Widgets built in this way, such as Text, Container, Row, Column, etc., do not depend on any other information at the time of creation except for these configuration parameters. In other words, once they are created successfully, they do not care about or respond to any data changes for redrawing. In Flutter, such widgets are called StatelessWidget.

Here is an illustration of a StatelessWidget:

Figure 1: StatelessWidget illustration

Next, I will use the source code of the Text widget as an example to explain the construction process of StatelessWidget.

class Text extends StatelessWidget {     
  // Constructor and property declaration section
  const Text(this.data, {
    Key key,
    this.textAlign,
    this.textDirection,
    // Other parameters
    ...
  }) : assert(data != null),
       textSpan = null,
       super(key: key);
       
  final String data;
  final TextAlign textAlign;
  final TextDirection textDirection;
  // Other properties
  ...
  
  @override
  Widget build(BuildContext context) {
    ...
    Widget result = RichText(
       // Initial configuration
       ...
      )
    );
    ...
    return result;
  }
}

As you can see, after assigning the property list in the constructor, the build method initializes the child component RichText through its property list (such as the text data, alignment textAlign, and text direction textDirection), and then returns it. After that, Text does not respond to external data changes.

So, when should we use StatelessWidget?

Here, I have a simple judgment rule: Can the parent widget completely control the UI display effect through initialization parameters? If yes, then we can use StatelessWidget to design the constructor interface.

I have prepared two simple examples to help you understand this judgment rule.

The first example is that I need to create a custom popup control to prompt users with error messages that may occur during app usage. The parent widget of this component can completely pass the style information and error message needed by the widget to it during initialization. This means that the parent widget can completely control its display effect through initialization parameters. Therefore, I can use StatelessWidget to customize the component.

The second example is that I need to define a counter button. Each time the user clicks the button, the button color will darken. As you can see, the parent widget of this component can only control the initial display effect of the child widget and cannot control the color change that occurs during interaction. Therefore, I cannot use StatelessWidget to customize the component. In this case, StatefulWidget comes into play.

StatefulWidget #

Contrary to StatelessWidget, there are some widgets (such as Image, Checkbox) that not only display the static configuration passed in by the parent widget during initialization, but also need to handle user interactions (such as button clicks) or changes in their internal data (such as network data response) and reflect them in the UI.

In other words, after these widgets are created, they still need to listen and respond to data changes in order to repaint. In Flutter, these widgets are called StatefulWidget. The following is an illustration of a StatefulWidget:

Figure 2 StatefulWidget illustration

At this point, you may be a little confused. Because in my previous article “Widget, the cornerstone of building a Flutter UI”, I shared with you that widgets are immutable and need to be destroyed and rebuilt when they change, so there is no concept of state. So, what’s going on here?

In fact, StatefulWidget is implemented by using a State class as a proxy for widget construction. Next, I will use the partial source code of the Image widget as an example to explain the construction process of StatefulWidget and help you understand this concept.

Like the Text widget mentioned above, the Image widget’s constructor accepts property parameters to be used by this class. However, unlike the Text widget, the Image widget does not have a build method to create a view, but instead creates a state object of type _ImageState through the createState method, and this object is responsible for building the view.

This state object holds and handles the state changes of the Image widget, so I will use the _imageInfo property as an example to explain it to you.

The _imageInfo property is used to load the real image for the widget. Once the state object detects a change in the _imageInfo property through the _handleImageChanged method, it immediately calls the setState method of the _ImageState class and notifies the Flutter framework, “The data here has changed, please reload the image using the updated _imageInfo data!”. Then the Flutter framework marks the view state and updates the UI.

class Image extends StatefulWidget {
  //Constructor and property declarations
  const Image({
    Key key,
    @required this.image,
    //other parameters
  }) : assert(image != null),
       super(key: key);

  final ImageProvider image;
  //other properties
  ...
  
  @override
  _ImageState createState() => _ImageState();
  ...
}

class _ImageState extends State<Image> {
  ImageInfo _imageInfo;
  //other properties
  ...

  void _handleImageChanged(ImageInfo imageInfo, bool synchronousCall) {
    setState(() {
      _imageInfo = imageInfo;
    });
  }
  ...
  @override
  Widget build(BuildContext context) {
    final RawImage image = RawImage(
      image: _imageInfo?.image,
      //other initialization configurations
      ...
    );
    return image;
  }
 ...
}

As you can see in this example, Image runs in a dynamic way: listening for changes and updating the view. Unlike StatelessWidget, which controls UI display entirely through the parent widget, StatefulWidget only defines its initial state through the parent widget, and its own view state needs to be handled and UI display updated accordingly.

Now, you may ask, since StatefulWidget can respond to state changes and show static UI, is there still a need for StatelessWidget, which can only show static UI?

StatefulWidget is not a cure-all, use it with caution #

For UI frameworks, the same display effect can generally be achieved through multiple controls. From the definition, StatefulWidget seems to be omnipotent and it seems reasonable to replace StatelessWidget. As a result, the misuse of StatefulWidget can easily become a matter of course and is difficult to avoid.

However, the fact is that the misuse of StatefulWidget directly affects the rendering performance of Flutter applications.

Next, at the end of this article today, I will review the update mechanism of Widgets with you again to help you realize the cost of using StatefulWidget exclusively:

Widgets are immutable, and updating means destruction and rebuilding (build). StatelessWidget is static and once created, it does not need to be updated. However, for StatefulWidget, calling the setState method in the State class to update data will trigger the destruction and rebuilding of the view, and indirectly trigger the destruction and rebuilding of each of its child Widgets.

So, what does this mean?

If our root layout is a StatefulWidget, every time we update the UI in its State, it will result in the destruction and rebuilding of all Widgets on the page.

In the previous article, we learned that although Flutter internally uses the Element layer to minimize modifications to the actual rendered view and improve rendering efficiency, instead of destroying and rebuilding the entire RenderObject tree. However, the destruction and rebuilding of a large number of Widget objects cannot be avoided. If the rebuilding of a certain child Widget involves time-consuming operations, the rendering performance of the page will drop sharply.

Therefore, accurately evaluating your view display requirements and avoiding unnecessary StatefulWidget usage are the simplest and most direct ways to improve the rendering performance of Flutter applications.

In the next article, “Why do we need to do state management and how to do it?”, I will continue to teach you several common state management methods for StatefulWidget and discuss the basic principles of selecting which Widget to use in different scenarios. You can apply these principles to your subsequent work according to actual needs.

Summary #

Well, that’s all for today’s introduction to StatelessWidget and StatefulWidget. Let’s review the main points we covered today.

First, I introduced you to Flutter’s declarative UI programming paradigm and we learned about the basic design ideas of StatelessWidget and StatefulWidget by reading the source code of two typical widgets (Text and Image).

Since widgets are built from top to bottom, from parent to child, when we customize our own components, we can determine whether to inherit StatelessWidget or StatefulWidget based on the basic principle of whether the parent widget can completely control its UI display effect through initialization parameters.

Then, regarding the “cure-all” misconception of StatefulWidget, I reviewed the UI update mechanism of widgets with you. Although Flutter tries to minimize modifications to the actual rendered views through the Element layer, a large number of widget destruction and reconstruction cannot be avoided. Therefore, avoiding the misuse of StatefulWidget is the simplest and most direct way to improve application rendering performance.

It is worth noting that besides manually refreshing the UI through State, the build method of widgets may also be executed multiple times in some special scenarios. Therefore, we should avoid placing time-consuming operations inside this method. In the next article, “Speaking of lifecycle, what are we talking about?”, I will analyze in detail when and why the build method is executed.

Thought Exercise #

The Flutter project application template is a counter example application demo, with the root widget being a StatelessWidget. Please modify this demo to have the root widget as a StatefulWidget while maintaining the original functionality. Can you determine the performance difference between these two approaches through data profiling?

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 you are welcome to share this article with more friends to read together.