Previous related content

  • Flutter State Management, State Management comprehensive analysis: www.jianshu.com/p/9334b8f68…
  • Source code for Flutter Provider: juejin.cn/post/684490…
  • Everything about Flutter is Widget: juejin.cn/post/687332…

preface

In the last installment, we wrote the implementation of a Widget and got to know the Element. After a series of analysis, we have a better understanding of the Widget and Element. In this installment, we will have a deeper understanding of State. So why did the authorities design a Widget with State? Where does the State lifecycle come from? Why can State update the UI? With some doubt, how about we write a Widget with State instead of analyzing the source code directly? Let’s make a Widget with State that has a lifecycle and the ability to update the UI.

Main Contents of this time

  • What’s state at the macro level? What’s state at the micro level
  • State class inheritance diagram
  • Write a Widget with State by hand

What’s state at the macro level? What’s state at the micro level

At a macro level, the UI of flutter is declarative. Why declarative? From Win32 to the Web to Android and Ios, they are all imperative programming styles, as follows:

//android
TextView tv = TextView()
tv.setText("text")
Copy the code

When a UI changes, you have to call a setText to do so. But flutter does the opposite. It allows the developer to focus only on the current state of the application, and allows the framework to automatically render that state on the UI via functions.

  • Developers only care about state changes, architecturally separating UI from data
  • To go further, the real UI objects of flutter are RenderObjects, while the widgets remain the same. Every time the UI is refreshed, a new sub-widget tree is built and filtered by Element. The RenderObject is only slightly changed, improving rendering efficiency.

So what are the downsides?

  • Poor state management, resulting in frequent builds of the entire page
  • Adding state calculations to the Widget tree can lead to confusion and inconsistency in state management

Ideally, it would look like this: UI= f(state) for example:

class TestState extends StatefulWidget {
  @override
  _TestState createState() => _TestState();
}

class _TestState extends State<TestState> {
  FunState _funState = FunState();

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(
        children: [
          Text(_funState.state + "state"), /// Is not recommended
          Text(_funState.getState()) /// Recommended writing, f(state)],),); }}class FunState {
  String state = "state";
  getState() {
    return state + "Test"; }}Copy the code

See, we don’t recommend that

Text(_funState.state + "state"), /// Is not recommended
Copy the code

So that’s what I mean by macro state, but let’s just define it a little bit: State is basically a reflection of the current state of the UI. What about the micro? As we all know, each StatefulWidget has a corresponding State. Last time we learned about widgets, we learned that widgets actually display UI through Element. So what role is State? What does it do? Or why did Google design it this way? Let’s take our time to reveal the answer and conclude.

State class inheritance diagram

As I commonly used Form, FormField, Overlay, Scaffold widgets, they would correspond to a State of my own, of course, there are deeper inheritance relationships such as AnimatedWidgetBaseState, but its subclasses are private. Based on the class inheritance relationship, it is generally understood that the State class does not need a particularly deep inheritance relationship, which is slightly simpler than Widget and Element. The design idea of Flutter is that combination is greater than inheritance, so this is also the feature of the whole UI framework. This is the main reason class diagrams are so simple.

Write a Widget with State by hand

We’ll implement it the same way we did last time, inheriting the lowest level Widget, this time adding a State to disguise it as a StatefulWidget. Here we go.

class StateWidget extends Widget{
  /// The constructor
  const StateWidget({ Key key }) : super(key: key);
  
  @override
  Element createElement() {
    // TODO: implement createElement
    throwUnimplementedError(); }}Copy the code

Create the StateWidget class that inherits from the Widget that lets us implement an Element, so let’s create another Element, this time using ComponentElement. We used Element last time, which is a little bit harder to implement. In this video we’re going to look at State, right, so we’re going to inherit ComponentElement to quickly implement and understand State

class StateWidget extends Widget{
  /// The constructor
  const StateWidget({ Key key }) : super(key: key);

  @override
  Element createElement() {
    return StateElement(this); }}class StateElement extends ComponentElement{
  
  StateElement(Widget widget) : super(widget);

  @override
  Widget build() {
    
  }
}
Copy the code

We remember that State has a lot of properties and functions. What are those? See figure

So let’s emulate that, and define these functions as follows

abstract class States<T extends StateWidget> {

  T get widget => _widget;
  T _widget;

  BuildContext get context => _element;
  StateElement _element;
  
  @protected
  @mustCallSuper
  void initState() {}

  @protected
  @mustCallSuper
  void didUpdateWidget(covariant T oldWidget) {}

  @protected
  @mustCallSuper
  void reassemble() {}

  @protected
  @mustCallSuper
  void setState(VoidCallback fn) {}

  @protected
  @mustCallSuper
  void deactivate() {}

  @protected
  @mustCallSuper
  void dispose() {}

  @protected
  Widget build(BuildContext context);

  @protected
  @mustCallSuper
  void didChangeDependencies() { }

}
Copy the code

The protected keyword should be the same as the Java scope. MustCallSuper makes it mandatory for subclass implementations to call super. The current function, of course, has debugFillProperties and other debug-related functions, which we will not care about for now. Let’s fix the core problem first. Next, how to connect States to widgets? Think about how we used it before?

  @override
  _TestState createState() => _TestState();
Copy the code

Yes, the Widget has a createState function, which we can add as follows:

abstract class StateWidget extends Widget {
  /// The constructor
  const StateWidget({Key key}) : super(key: key);

  @override
  Element createElement() {
    return StateElement(this);
  }

  @protected
  @factory
  States createState();
}
Copy the code

Factory annotation meaning: Used to annotate instances or static methods. Indicates that the method is either abstract or must return a newly allocated object or “NULL”. In addition, each implementation or override of the method is implicitly annotated with the same annotations.

The Widget is already complete. What about Element? Let’s take a look at Element’s code

class StateElement extends ComponentElement {

  ///Here, change the previous Widget to the StateWidget to prevent coercion
  StateElement(StateWidget widget) : super(widget);

  @override
  Widget build() {

  }
}
Copy the code

State inherits from ComponentElement, overrides build, and happens to have an abstract function called build, so it must be here

class StateElement extends ComponentElement {

  States<StateWidget> get state => _state;
  States<StateWidget> _state;

  StateElement(StateWidget widget) : super(widget);

  @override
  Widget build() {
    return _state.build(this); }}Copy the code

Cache State in Element and call _state.build(this) in build, which is the familiar BuildContext, and BuildContext instance is the current Element object. Now you can see that _state is not assigned, right? It is returned by the createState function in the widget. So when should we call it? To avoid it createState multiple times, wouldn’t it be more appropriate in a constructor? Put it in as follows

class StateElement extends ComponentElement {

  States<StateWidget> get state => _state;
  States<StateWidget> _state;

  StateElement(StateWidget widget)
        ///Create the State
      : _state = widget.createState(),
        super(widget){
    ///Assertion judgment
    assert(_state._element == null);
    ///Assign to the element in State, which is the context you get in State
    _state._element = this;
    assert(_state._widget == null);
    ///Widget assignment in state
    _state._widget = widget;
  }

  @override
  Widget build() {
    return _state.build(this); }}Copy the code

The constructor assigns values to all elements and widgets in State, followed by State’s initState() function, which is a common initialization function. When is it called on element? Or when is it appropriate to call it? Well, first of all, it’s only going to be called once, it’s not going to be initialized twice, that doesn’t make sense, anyone say put it in a construct? Let’s look at the life cycle of Element

After createElement is called, mount is called when the Element is actually mounted to the tree. If the Element is not mounted to the UI at all, is there no need to initialize it? Is it suitable for construction? Not suitable, right? So the implementation is as follows:

class StateElement extends ComponentElement {

  States<StateWidget> get state => _state;
  States<StateWidget> _state;
  bool isNeedInit;

  StateElement(StateWidget widget)
        ///Create the State
      : _state = widget.createState(),
        super(widget){
    ///Assertion judgment
    assert(_state._element == null);
    ///Assign to the element in State, which is the context you get in State
    _state._element = this;
    assert(_state._widget == null);
    ///Widget assignment in state
    _state._widget = widget;
  }

  @override
  void mount(Element parent, newSlot) {
    super.mount(parent, newSlot);
    assert(isNeedInit == null);
    _state.initState();
    isNeedInit = false;
  }

  @override
  Widget build() {
    return _state.build(this); }}Copy the code

The initState function is called in the mount function and is controlled to be called only once by the isNeedInit variable. And then the didChangeDependencies function, why do I call it? Subclasses rarely override this method because the framework always calls build after a dependency has changed. Some subclasses do override this approach because they do some expensive work (for example, network fetching) when their dependencies change, and the work is too expensive for each build.

Detailed understanding please see bosses analysis: www.jianshu.com/p/9cb6c57b7…

< span style = “box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; font-size: 14px! Important; word-break: inherit! Important;” The system calls Element’s didChangeDependencies and executes State’s didChangeDependencies as follows:

class StateElement extends ComponentElement {

  States<StateWidget> get state => _state;
  States<StateWidget> _state;
  bool isNeedInit;

  bool _didChangeDependencies = false;


  StateElement(StateWidget widget)
        ///Create the State
      : _state = widget.createState(),
        super(widget){
    ///Assertion judgment
    assert(_state._element == null);
    ///Assign to the element in State, which is the context you get in State
    _state._element = this;
    assert(_state._widget == null);
    ///Widget assignment in state
    _state._widget = widget;
  }

  @override
  void mount(Element parent, newSlot) {
    super.mount(parent, newSlot);
    assert(isNeedInit == null);
    _state.initState();
    ///The Widget changes from Null to a concrete Widget when it is first loaded
    _state.didChangeDependencies();
    isNeedInit = false;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    ///Element's widgets have definitely changed
    _didChangeDependencies = true;
  }
  
  @override
  void performRebuild() {
    ///At this point it is necessary to execute State's didChangeDependencies
    if (_didChangeDependencies) {
      _state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }

  @override
  Widget build() {
    return _state.build(this); }}Copy the code

State’s didChangeDependencies are executed in element’s performRebuild function if element’s didChangeDependencies function is executed in the system.

Now let’s move on to the Reassemble function, what does that function do? This callback is provided specifically for development debugging and is called on hot reload, never in Release mode. We don’t need to worry too much about this. We can call it directly from Element.

  @override
  void reassemble() {
    _state.reassemble();
    super.reassemble();
  }
Copy the code

When a widget is rebuilt, the Flutter framework calls Widget.canUpdate to detect the old and new nodes at the same location in the widget tree and decide whether an update is needed. This callback is called if widget.canupdate returns true. As mentioned earlier, widget.canupdate will return true if the new and old widgets’ key and runtimeType are equal, which means didUpdateWidget() will be called when the new and old widgets’ key and runtimeType are equal. But you notice that Element doesn’t have this function. It only has the update function. And then I looked at the implementation of StatefulElement which is the didUpdateWidget called here, so let’s implement it and see what the logic is

class StateElement extends ComponentElement {
  States<StateWidget> get state => _state;
  States<StateWidget> _state;
  bool isNeedInit;

  bool _didChangeDependencies = false;

  StateElement(StateWidget widget)

      ///Create the State
      : _state = widget.createState(),
        super(widget) {
    ///Assertion judgment
    assert(_state._element == null);

    ///Assign to the element in State, which is the context you get in State
    _state._element = this;
    assert(_state._widget == null);

    ///Widget assignment in state
    _state._widget = widget;
  }

  @override
  void mount(Element parent, newSlot) {
    super.mount(parent, newSlot);
    assert(isNeedInit == null);
    _state.initState();

    ///The Widget changes from Null to a concrete Widget when it is first loaded
    _state.didChangeDependencies();
    isNeedInit = false;
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();

    ///Element's widgets have definitely changed
    _didChangeDependencies = true;
  }

  @override
  void performRebuild() {
    ///At this point it is necessary to execute State's didChangeDependencies
    if (_didChangeDependencies) {
      _state.didChangeDependencies();
      _didChangeDependencies = false;
    }
    super.performRebuild();
  }

  @override
  void reassemble() {
    _state.reassemble();
    super.reassemble();
  }

  @override
  void update(Widget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);

    /// Get the old widgets first
    final StateWidget oldWidget = _state._widget;

    /// Force the state to be updatable
    markNeedsBuild();

    /// Update State's Widget to the new Widget
    _state._widget = widget as StateWidget;
    /// The callback_state.didUpdateWidget
    _state.didUpdateWidget(oldWidget);
    /// Call the rebuild function and finally call performRebuild to update Element
    rebuild();
  }

  @override
  Widget build() {
    return _state.build(this); }}Copy the code

If the update function is implemented as above, you will notice that the ditUpdateWidget parameter is an old Widget. The update function is finished. Deactivate is called when the State object is removed from the tree. The Flutter framework can re-insert a State object into the tree in some scenarios, such as when the subtree containing the State object moves from one position in the tree to another (this can be done using a GlobalKey). If it is not re-inserted into the tree after removal, the Dispose () method is immediately called. Looking at the implementation is simple:

  @override
  void deactivate() {
  	/// No special treatment is required
    _state.deactivate();
    super.deactivate();
  }
Copy the code

Dispose dispose means to release. When was the Element released? That must be unmount, so it goes without saying that the implementation is as follows:

  @override
  void unmount() {
    super.unmount();
    _state.dispose();
    /// To NULL to release the call reference
    _state._element = null;
    _state = null;
  }
Copy the code

The setState function is the core function that Widgets can recreate. The setState function is the core function that widgets can recreate. The setState function is the core function that Widgets can recreate.

abstract class States<T extends StateWidget> {
  T get widget => _widget;
  T _widget;

  BuildContext get context => _element;
  StateElement _element;

  @protected
  @mustCallSuper
  void initState() {}

  @protected
  @mustCallSuper
  void didUpdateWidget(covariant T oldWidget) {}

  @protected
  @mustCallSuper
  void reassemble() {}

  @protected
  @mustCallSuper
  void setState(VoidCallback fn) {
    assert(fn ! =null);
    ///. State judgment is omitted
    final dynamic result = fn() as dynamic;
    assert(() {
      if (result is Future) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('setState() callback argument returned a Future.'),
          ErrorDescription(
              'The setState() method on $this was called with a closure or method that '
                  'returned a Future. Maybe it is marked as "async".'
          ),
          ErrorHint(
              'Instead of performing asynchronous work inside a call to setState(), first '
                  'execute the work (without updating the widget state), and then synchronously '
                  'update the state inside a call to setState().'),]); }// We ignore other types of return values so that you can do things like:
      // setState(() => x = 3);
      return true; } ());/// The most important one, markNeedsBuild, is to have the updatable state of an Element wait for the framework layer to refresh.
    _element.markNeedsBuild();
  }

  @protected
  @mustCallSuper
  void deactivate() {}

  @protected
  @mustCallSuper
  void dispose() {}

  @protected
  Widget build(BuildContext context);

  @protected
  @mustCallSuper
  void didChangeDependencies() {}
}
Copy the code

It first checks whether fn is null, and then adds a state check. The next step is the Future handling of the fn function, and the last call is _element.markNeedsbuild (); That’s the key to our Widget rebuild. It’s basically a flag bit _dirty. When you set it to true, the framework layer knows that it’s updating and will perform the update in response. You can update your status.

Can we use States by ourselves? Let’s try it out. The code is as follows:

class TestStateWidget extends StateWidget {
  @override
  States<StateWidget> createState() {
    return TestStates();
  }
}

class TestStates extends States<TestStateWidget> {

  String data;
  int num = 0;

  @override
  void initState() {
    data = "123";
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(8),
      child: Column(
        children: [
          Text(data),
          MaterialButton(
            onPressed: () {
              setState(() {
                data = "456${num++}";
              });
            },
            child: Text("更新"),
          )
        ],
      ),
    );
  }
}
Copy the code

Dart into main.dart:

@override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Column( children: [ // Center( // // Center is a layout widget. It takes a single child and positions it // // in the middle of the parent. // child: isShow ? const TestWidget() : Container(), // ), // MaterialButton( // onPressed: () { // showialog(context); // }, // child: Text('showDialog'), // ), // Center( // // Center is a layout widget. It takes a single child and positions it // // in the middle of the parent. // child: isShow // ? TestWidget( // key: key, // ) // : Container( // padding: EdgeInsets.all(9), // child: TestWidget( // key: key, // ), // ), // ), TestStateWidget() ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { isShow = ! isShow; }); }, tooltip: 'Increment', child: Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); }Copy the code

Operation effect:

Click on the update

Then click on

Okay, so it’s done, it’s done fine, it’s implemented a set of widgets with State, right? So let’s conclude.

conclusion

Life cycle summary a diagram

  • The State is derived from Element, which is more properly called a mediator or delegate
  • Context can be Element, but State cannot, because they are not inherited.
  • The importance of mastering Element comes back, because State’s functionality is limited to Element.
  • Does State act as a VM in the MVVM architecture? I think you could say that.

Wait a minute, okay, this is the end of the story, looking forward to your approval, a like on the line. Thank you