[Flutter Advanced] Talk about the life cycle, state management and partial redrawing in components

foreword

When it comes to life cycle, those who are familiar with Android development must immediately think of the life cycle of Activity. Since everything is a component in Flutter, the life cycle of components is actually similar.

In this process, the state of the component - State is very important. It records the state of the variable part of the entire component. When the state changes, the component needs to be refreshed to display the latest state.

Of course, if the component is more complex, it is not advisable for one of the state changes to cause the refresh of the entire component. This involves the partial redrawing or partial refresh mechanism in Flutter.

This article uses these three aspects to explain in detail the relevant mechanisms in Flutter and how to use them.

life cycle

There are actually two life cycles of flutter: StatefulWidget and StatelessWidget.

These two are the two basic components of flutter, and the names already indicate the functions of these two components: stateful and stateless.

  1. StatelessWidget

StatelessWidget is a stateless component, its life cycle is very simple, there is only one build, as follows:

class WidgetA extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return ...;
  }
}

For StatelessWidget it is only rendered once, after that it doesn't change any more.

Since stateless components have only one build phase during execution, only one build function will be executed during execution, and there are no other lifecycle functions, so they are better than stateful components in terms of execution speed and efficiency. Therefore, when designing components, consider the business situation and try to use stateless components.

  1. StatefulWidget

StatelessWidget is a stateful component, and the life cycle we discuss basically refers to its cycle, as shown in the figure:

image.png

Contains the following stages:

  • createState: This function is the method of creating State in StatefulWidget, when StatefulWidget is called, createState will be executed immediately.

  • initState: This function is called for State initialization, so the initial assignment of State variables can be performed during this period, and it can also interact with the server during this period , and call setState to set the State after obtaining server data.

  • didChangeDependencies: This function is used when the state that the component depends on changes. The state mentioned here is the global state, such as language or theme, which is similar to the state stored in the front-end Redux.

  • build: It mainly returns the Widget that needs to be rendered. Since build will be called multiple times, it can only return Widget-related logic in this function to avoid status abnormalities caused by multiple executions. Pay attention to performance issues here .

  • reassemble: It is mainly used in the development stage. In debug mode, this function will be called every time hot reloading, so you can add some debug code during the debug stage to check code problems.

  • didUpdateWidget: This function is mainly used when the component is rebuilt, such as hot overloading, and the parent component is built, the method of the child component will be called, and then the build method in this component will be called after the method is called.

  • deactivate: It will be called after the component is removed from the node. If the component is removed from the node and then not inserted into other nodes, it will continue to call dispose to remove it permanently.

  • dispose: Permanently remove the component and release the component resources.

In StatelessWidget, as long as we call setState, redrawing will be performed, that is to say, the build function will be re-executed, so that the ui can be changed.

component refresh

Let's take a look at the code below:

class MyHomePage extends StatefulWidget {
    
    

  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
    
    
  int _counter = 0;

  void _incrementCounter() {
    
    
    setState(() {
    
    
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            WidgetA(_counter),
            WidgetB(),
            WidgetC(_incrementCounter)
          ],
        ),
      ),
    );
  }
}

class WidgetA extends StatelessWidget {
    
    
  final int counter;

  WidgetA(this.counter);

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Text(counter.toString()),
    );
  }
}

class WidgetB extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Text('I am a widget that will not be rebuilt.');
  }
}

class WidgetC extends StatelessWidget {
    
    
  final void Function() incrementCounter;

  WidgetC(this.incrementCounter);

  
  Widget build(BuildContext context) {
    
    
    return RaisedButton(
      onPressed: () {
    
    
        incrementCounter();
      },
      child: Icon(Icons.add),
    );
  }
}

We have three Widgets, one is responsible for displaying the count, one is for changing the count with a button, and the other is for displaying text statically. Through these three Widgets, we can compare and compare the refreshing logic of the page.

In the above code, the three Widgets are created in the build of _MyHomePageState. After execution, click the button to find that all three Widgets are refreshed.

You can see it by selecting Track Widget Rebuilds on the Flutter Performance panel

image.png

Although the three Widgets are stateless StatelessWidgets, the build function will be re-executed when the State of _MyHomePageState changes, so the three Widgets will be recreated, which is why WidgetA can still be dynamically changed even though it is a stateless StatelessWidget .

So: stateless StatelessWidget cannot be changed dynamically, but cannot be changed internally through State, but when the State of its parent Widget changes, its construction parameters can be changed to make it change. In fact, it cannot be changed, because it is a new instance.

Next, we will create three components in advance, which can be created in the constructor of _MyHomePageState. The modified code is as follows:

class _MyHomePageState extends State<MyHomePage> {
    
    
  int _counter = 0;
  List<Widget> children;

  _MyHomePageState(){
    
    
    children = [
      WidgetA(_counter),
      WidgetB(),
      WidgetC(_incrementCounter)
    ];
  }

  void _incrementCounter() {
    
    
    setState(() {
    
    
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: children,
        ),
      ),
    );
  }
}

Execute it again, and find that the click has no effect, and you can see that there is no Widget refresh on Flutter Performance (this refers to three Widgets, of course, the Scaffold is still refreshed).

This is because the components are created in advance, so the three Widgets are not recreated when the build is executed, so the content displayed by WidgetA has not changed, because its counter has not been re-introduced.

Therefore, components that do not need to be dynamically changed can be created in advance, and can be used directly during build, while components that need to be dynamically changed are created in real time.

Is it possible to achieve partial refresh in this way? We continue to change the code as follows:

class _MyHomePageState extends State<MyHomePage> {
    
    
  int _counter = 0;
  Widget b = WidgetB();
  Widget c ;

  _MyHomePageState(){
    
    
    c = WidgetC(_incrementCounter);
  }

  void _incrementCounter() {
    
    
    setState(() {
    
    
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            WidgetA(_counter),
            b,
            c
          ],
        ),
      ),
    );
  }
}

We only recreate WidgetB and WidgetC, while WidgetA is created in build. After execution, the content of WidgetA is changed when the button is clicked. If you check Flutter Performance, you can see that only WidgetA is refreshed, and WidgetB and WidgetC are not refreshed.

image.png

So: By creating static components in advance and using them directly when building, and creating dynamic Widgets directly when building, this method can achieve partial refresh.

Notice:

As long as setState, _MyHomePageState will be refreshed, so WidgetA will be refreshed even if the count has not changed. For example, comment out the _count++ code in setState in the above code, and then click the button, although the content has not changed, but WidgetA is still refreshed.

This situation can be optimized through InheritedWidget.

InheritedWidget

What is the role of InheritedWidget? Some people on the Internet say it is for data sharing, while others say it is for partial refresh. Let's look at the official description:

Base class for widgets that efficiently propagate information down the tree.

It can be seen that its function is to effectively transmit messages from the top to the bottom of the Widget tree, so many people understand it as data sharing, but pay attention to this "effectiveness", this is its key, and this effectiveness is actually to solve the above mentioned problems. to the problem.

So how is it used?

First create a class that inherits from InheritedWidget:

class MyInheriteWidget extends InheritedWidget{
    
    
  final int count;
  MyInheriteWidget({
    
     this.count, Widget child}) : super(child: child);

  static MyInheriteWidget of(BuildContext context){
    
    
    return context.dependOnInheritedWidgetOfExactType<MyInheriteWidget>();
  }

  
  bool updateShouldNotify(MyInheriteWidget oldWidget) {
    
    
    return oldWidget.count != count;
  }
}

Here count is passed in. It is important to realize the updateShouldNotify function. You can know from the name that this function determines whether the Child Widget of the InheritedWidget needs to be refreshed. Here we judge that it will be refreshed if it has changed from before. This solves the problem mentioned above.

Then we need to implement a static of method, which is used to obtain the InheritedWidget in the Child Widget, so that its count attribute can be accessed, which is message passing, the so-called data sharing (because the child of the InheritedWidget can be a layout, inside There are multiple widgets, and these widgets can use the data in this InheritedWidget).

Then we transform WidgetA:

class WidgetA extends StatelessWidget {
    
    

  
  Widget build(BuildContext context) {
    
    
    final MyInheriteWidget myInheriteWidget = MyInheriteWidget.of(context);
    return Center(
      child: Text(myInheriteWidget.count.toString()),
    );
  }
}

This time, you don’t need to pass the count in the constructor, you can directly get MyInheriteWidget through of and use its count.

Finally modify _MyHomePageState:

class _MyHomePageState extends State<MyHomePage> {
    
    
  int _counter = 0;
  Widget a = WidgetA();
  Widget b = WidgetB();
  Widget c ;

  _MyHomePageState(){
    
    
    c = WidgetC(_incrementCounter);
  }

  void _incrementCounter() {
    
    
    setState(() {
    
    
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            MyInheriteWidget(
              count: _counter,
              child: a,
            ),
            b,
            c
          ],
        ),
      ),
    );
  }
}

Note that WidgetA is wrapped with MyInheriteWidget here, and WidgetA must be created in advance. If it is created in build, it will be refreshed every time MyInheriteWidget is refreshed, so the effect of updateShouldNotify function cannot be achieved.

Execute, click the button, you can find that only WidgetA is refreshed (of course MyInheriteWidget is also refreshed). If you comment out the _count++ code in setState, execute it again and click to find that although MyInheriteWidget is refreshed, WidgetA is not refreshed because the count of MyInheriteWidget has not changed.

Let's change the code and put WidgetB and C into MyInheriteWidget, what will happen?

 
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            MyInheriteWidget(
              count: _counter,
              child: Column(
                children: [
                  a,
                  b,
                  c
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

The child of MyInheriteWidget is a Column, and put a, b, and c under it. Execution will find that WidgetA is still refreshed, and neither B nor C is refreshed. This is because the of function of MyInheriteWidget is not executed in B and C, and dependOnInheritedWidgetOfExactType is not executed, so there is no dependency, and MyInheriteWidget will not notify them.

If we modify WidgetC and add a line of MyInheriteWidget.of(context) to the build function; then although there is no use, it will still be refreshed, because it will be notified when the dependency is established.

InheritedWidget will solve the redundant refresh problem. For example, if there are multiple properties in a page, there are also multiple Widgets to use these properties, but not every Widget uses all properties. If you use the most common implementation, you need to refresh these Widgets every time you setState (no matter which property is changed). But if we use multiple InheritedWidgets to classify these Widgets, use the same attribute to wrap with the same InheritedWidget, and implement updateShouldNotify, so that when one of the attributes is changed, only the InheritedWidget related to the attribute will refresh its child, so It improves performance.

InheritedModel

InheritedModel is inherited from InheritedWidget, which expands its functions, so its functions are more powerful. Where exactly?

We know from the above that InheritedWidget can decide whether to refresh the child by judging whether its data changes, but in reality, the data can be multiple variables or a complex object, and the child is not a single widget, but a series of widget combinations . For example, to display a book, the data may include title, serial number, date, etc., but each data may change independently. If you use InheritedWidget, you need an InheritedWidget class for each type of data, and then wrap the widget that uses the data. In this way, other widgets will not be refreshed when the package changes a certain data.

But such a problem is that the widget level is more complicated and confusing, and InheritedModel can solve this problem. The biggest function of InheritedModel is to refresh different widgets according to different data changes. Let's see how to do it.

First create an InheritedModel:

class MyInheriteModel extends InheritedModel<String>{
    
    
  final int count1;
  final int count2;
  MyInheriteModel({
    
     this.count1,  this.count2, Widget child}) : super(child: child);

  static MyInheriteModel of(BuildContext context, String aspect){
    
    
    return InheritedModel.inheritFrom(context, aspect: aspect);
  }

  
  bool updateShouldNotify(MyInheriteModel oldWidget) {
    
    
    return count1 != oldWidget.count1 || count2 != oldWidget.count2;
  }

  
  bool updateShouldNotifyDependent(MyInheriteModel oldWidget, Set<String> dependencies) {
    
    
    return (count1 != oldWidget.count1 && dependencies.contains("count1")) ||
        (count2 != oldWidget.count2 && dependencies.contains("count2"));
  }
}

Here we pass in two counts. In addition to implementing the updateShouldNotify method, we also need to implement the updateShouldNotifyDependent method. This function is the key. We can see that after we judge whether a certain data has changed, we also judge whether dependencies contain a keyword:

count1 != oldWidget.count1 && dependencies.contains(“count1”)

What is this keyword? from where? It will be mentioned later, here is an impression first.

Then it is also necessary to implement a static of function to obtain this InheritedModel, the difference is that the code obtained here has changed:

InheritedModel.inheritFrom(context, aspect: aspect);

The aspect here is the keyword used later, and inheritFrom will put this keyword into dependencies so that updateShouldNotifyDependent can use it. The complete function of this aspect will be explained in detail later.

Then we transform WidgetA:

class WidgetA extends StatelessWidget {
    
    

  
  Widget build(BuildContext context) {
    
    
    final MyInheriteModel myInheriteModel = MyInheriteModel.of(context, "count1");
    return Center(
      child: Text(myInheriteModel.count1.toString()),
    );
  }
}

As you can see, aspect is defined here.

Then because there are two counts, we add two more Widgets to handle count2:

class WidgetD extends StatelessWidget {
    
    

  
  Widget build(BuildContext context) {
    
    
    final MyInheriteModel myInheriteModel = MyInheriteModel.of(context, "count2");
    return Center(
      child: Text(myInheriteModel.count2.toString()),
    );
  }
}

class WidgetE extends StatelessWidget {
    
    
  final void Function() incrementCounter;

  WidgetE(this.incrementCounter);

  
  Widget build(BuildContext context) {
    
    
    return RaisedButton(
      onPressed: () {
    
    
        incrementCounter();
      },
      child: Icon(Icons.add),
    );
  }
}

Here you can see that the aspect of WidgetD is different from WidgetA.

Finally modify _MyHomePageState:

class _MyHomePageState extends State<MyHomePage> {
    
    
  int _counter = 0;
  int _counter2 = 0;
  Widget a = Row(
    children: [
      WidgetA(),
      WidgetD()
    ],
  );
  Widget b = WidgetB();
  Widget c ;
  Widget e ;

  _MyHomePageState(){
    
    
    c = WidgetC(_incrementCounter);
    e = WidgetE(_incrementCounter2);
  }

  void _incrementCounter() {
    
    
    setState(() {
    
    
      _counter++;
    });
  }

  void _incrementCounter2() {
    
    
    setState(() {
    
    
      _counter2++;
    });
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            MyInheriteModel(
              count1: _counter,
              count2: _counter2,
              child: a,
            ),
            b,
            c,
            e
          ],
        ),
      ),
    );
  }
}

WidgetD and E handle count2, while A and C handle count. The child of MyInheriteModel is not a single Widget, but a Row, including WidgetD and A.

Execute the code, you can find that when WidgetC is clicked, only WidgetA is refreshed (of course MyInheriteModel is also refreshed); when WidgetD is clicked, only WidgetE is refreshed. In this way, we have realized the partial refresh in MyInheriteModel.

In fact, the principle is very simple. Aspect is equivalent to a mark. When we get MyInheriteModel through InheritedModel.inheritFrom(context, aspect: aspect);, we actually depend on this Widget to MyInheriteModel and mark this Widget. At this time, if the data changes, when traversing all its dependencies, it will obtain its corresponding tag set dependencies through each dependent Widget, and then trigger updateShouldNotifyDependent to determine whether the Widget is refreshed.

So there is a map in InheritedModel (actually InheritedElement), which records the dependencies corresponding to each dependent Widget, so a Widget can have multiple tags, because dependencies are a Set, so that it can respond to multiple data changes (such as Multiple data form a String to be displayed as text).

The above can actually be realized with two InheritedWidgets, but the more complex the layout, the more InheritedWidgets are needed, and it takes time and effort to maintain.

So you can see that InheritedModel is more flexible to use, more powerful, more suitable for complex data and layout, and refines each refresh area by subdivision, so that each refresh only updates the smallest area, which greatly improves performance.

InheritedNotifier

InheritedNotifier also inherits from InheritedWidget. It is a special tool for subclasses of Listenable. Its constructor needs to pass in a Listenable (this is an interface, not the previous data data), such as animation (such as AnimationController ), and then its dependent components are updated according to Listenable.

First, create an InheritedNotifier first:

class MyInheriteNotifier extends InheritedNotifier<AnimationController>{
    
    
  MyInheriteNotifier({
    
    
       Key key,
       AnimationController notifier,
       Widget child,
     }) : super(key: key, notifier: notifier, child: child);

  static double of(BuildContext context){
    
    
    return context.dependOnInheritedWidgetOfExactType<MyInheriteNotifier>().notifier.value;
  }
}

The of function provided here can directly return the value of AnimationController.

Then create a Widget:

class Spinner extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    return Transform.rotate(
      angle: MyInheriteNotifier.of(context) * 2 * pi,
      child: Text("who!!"),
    );
  }
}

The content is rotated according to the AnimationController.

Modify WidgetA:

class WidgetA extends StatelessWidget {
    
    

  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Text("WidgetA"),
    );
  }
}

Then modify _MyHomePageState:

class _MyHomePageState extends State<MyHomePage6> with SingleTickerProviderStateMixin {
    
    
  AnimationController _controller;

  
  void initState() {
    
    
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: Duration(seconds: 10),
    )..repeat();
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            WidgetA(),
            MyInheriteNotifier(
                notifier: _controller,
                child: Spinner()
            ),
          ],
        ),
      ),
    );
  }
}

Running will see that the Text is constantly rotating. Of course, if there are other Widgets that can be seen, they will not be refreshed.

In short, InheritedNotifier is a more detailed tool that focuses on a specific scene and is more convenient to use.

Notify

Finally, let’s briefly introduce Notifier. Consider a requirement: page A is a list page, while page B is a detail page. Both pages have a like operation and display the number of likes. After a page is liked, the data of the two pages needs to be shared at the same time. refresh. In this case, you can use flutter to provide another way - Notifier.

Notifier is actually the implementation of subscription mode, mainly including ChangeNotifier and ValueNotifier, and it is very simple to use. Subscribe and unsubscribe through addListener and removeListener (the parameter is a function with no parameters and no return value), and call notifyListeners(); notification when the data changes.

ValueNotifier is a simpler ChangeNotifier. It has only one data value, which can be directly set and get. When setting, notifyListeners() is automatically executed, so it is suitable for simple scenarios with single data.

At that time, it was noted that Notifier only shared data and notified changes, but did not implement refresh, so it had to be implemented in conjunction with others. For example, the above InheritedNotifier (because the Notifier inherits the Listenable interface, the two can be used together very easily), or the third-party library Provider (the habit of web development), etc.

Summarize

The life cycle, component state, partial redrawing and other mechanisms mentioned in this article are very important in the development of Flutter. Understanding relevant knowledge can not only make the code logic clearer, but also improve the fluency of the application and reduce the occurrence of bugs.

Flutter is characterized by cross-platform, another feature is mixed development, which involves interaction with native code, you can read these blogs:

[Flutter Advanced] Three ways to interact with Native

[Flutter Hybrid Development] Introducing Flutter into existing iOS projects

[Flutter Mixed Development] How to start Flutter in an Android project

Guess you like

Origin blog.csdn.net/chzphoenix/article/details/130132679