What is a ScopedModel

The ScopedModel was separated from the Fuchsia library, a new system being developed by Google, in order to better manage the states in the flutter when using the flutter. The ScopedModel was the first state management library to be used by flutter. Although it is no longer maintained, it is still used by many people, and learning the ScopedModel is an easy way to learn about flutter and understand the mechanism of state management in the flutter.

What is state management? In a nutshell when we build a project, maybe it’s easy to start with, just map some of the components to a view, let me show you a well-known diagram

As our project gets more complex, our program will have a lot of components and views and hundreds of states, and it will get more complicated if you pass them all through the children and the parents

This is when our states become complicated, and we may cry to maintain them. That’s where state management comes in.

Scoped_model provides the ability to pass the data model to all its descendants and re-render the descendants as needed.

Method of use

The use method is relatively simple, I use my own encapsulation Store as an example.

Check the operation method in the official introduction pub.dev/packag…

Check out the official example code github.com/brianega…

Introduction of depend on

. Scoped_model: ^1.0.1Copy the code

Pub. Dev /packages/sc…

Encapsulation Store

The Store class serves as the entry and exit for the ScopedModel. All operations related to the ScopedModel pass through this class, which has the advantages of clear responsibilities and easier maintenance.

Class MyStoreScoped {// We'll instantiate runAPP in main.dart init static init({context, child}) {returnScopedModel<Counter>( model: Counter(), child: ScopedModel<UserModel>( model: UserModel(), child: child, ), ); Static T value<T extends Model>(context) {return ScopedModel.of<T>(context, rebuildOnChange: true); } static ScopedModelDescendant CONNECT <T extends Model>({@Required Builder}) {returnScopedModelDescendant<T>(builder: builder); }}Copy the code

I’ve introduced two models here, Counter and UserModel, and I’m only using Counter in this example, just to give you an idea, if we have multiple models to introduce, we can put this in here, and if we have too many, we can write a recursive method to encapsulate it.

The following value and connect methods are used to get and manipulate model instances, as described later.

Create a Model

class Counter extends Model {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }

  void decrement() { count--; notifyListeners(); }}Copy the code

Top level introduction Model

@override Widget build(BuildContext context) {return MyStoreScoped.init(
        context: context,
        child: new MaterialApp(
        home: FirstPage(),
      ),
    );
  }
Copy the code

To obtain the Model

There are two ways to obtain and modify the value of a Model. The first is to use == scopedModel. of(context, rebuildOnChange: true)==

. Static T value<T extends Model>(context) {return ScopedModel.of<T>(context, rebuildOnChange: true); }...Copy the code

then

Widget build(BuildContext context) {
    print('second page rebuild');
    Counter model = MyStoreScoped.value<Counter>(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('SecondPage'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            RaisedButton(
              child: Text('+'),
              onPressed: () {
                model.increment();
              },
            ),
            Builder(
              builder: (context) {
                print('second page counter widget rebuild');
                return Text('second page: ${model.count}');
              },
            ),
            RaisedButton(
              child: Text(The '-'), onPressed: () { model.decrement(); },),],),); }Copy the code

When you click + and -, the middle number will change. However, it is important to note that when we use this method, any change in the data in the Model will cause the whole build to be rerendered because of the true passed by rebuildOnChange, and if the pages in the routing stack are also used in this way, the pages in the routing stack will also be rerendered. So the abuse of this way, to a certain extent will certainly cause the page performance is not good, the second way can be a good solution to this problem.

The second way is to use ==ScopedModelDescendant(Builder: Builder)==

  static ScopedModelDescendant connect<T extends Model>({@required builder}) {
    return ScopedModelDescendant<T>(builder: builder);
  }
Copy the code

use

@override
  Widget build(BuildContext context) {
    print('first page rebuild');
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Center(
        child: Column(
          children: <Widget>[
            MyStoreScoped.connect<Counter>(builder: (context, child, snapshot) {
              return RaisedButton(
                child: Text('+'), onPressed: () { snapshot.increment(); }); }), MyStoreScoped.connect<Counter>(builder: (context, child, snapshot) {print('first page counter widget rebuild');
              return Text('${snapshot.count}');
            }),
            MyStoreScoped.connect<Counter>(builder: (context, child, snapshot) {
              return RaisedButton(
                child: Text(The '-'), onPressed: () { snapshot.decrement(); }); }), MyStoreScoped.connect<UserModel>( builder: (context, child, snapshot) {print('first page name Widget rebuild');
              return Text('${MyStoreScoped.value<UserModel>(context).name}');
            }),
            TextField(
              controller: controller,
            ),
            MyStoreScoped.connect<UserModel>(
                builder: (context, child, snapshot) {
              return RaisedButton(
                child: Text('change name'), onPressed: () { snapshot.setName(controller.text); }); }),],),); }Copy the code

This approach is wrapped in ScopedModelDescendant, using the model as the third parameter returned by the builder. == scopedModel. of(context, rebuildOnChange: True)==, but it uses a Widget. The context returned by the build method of the Widget limits the area that needs to be rerendered to the Widget returned by the Builder. For complex pages and high performance pages, this method can greatly improve the performance of the application.

This example code is uploaded to my Github daily demo, the specific code to view github.com/xuzhongpeng…

Realize the principle of

A picture is worth a thousand words

ScopedModel has four important parts: Model, ScopedModel, AnimatedBuilder, and InheritedWidget

model

The Model class inherits from Listenable, which provides a notifyListeners() method

ScopedModel and AnimatedBuilder

When we use the ScopedModel to register the Model at the top level, we use an AnimatedBuilder class inside the ScopedModel that passes an instance of the Model as the first argument to the class. When we call notifyListeners() in the Model, Subcomponents under this class are rerendered.

.@override
  Widget build(BuildContext context) {
    returnAnimatedBuilder( animation: model, builder: (context, _) => _InheritedModel<T>(model: model, child: child), ); }...Copy the code
class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if(widget.listenable ! = oldWidget.listenable) { oldWidget.listenable.removeListener(_handleChange); widget.listenable.addListener(_handleChange); } } @override voiddispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already. }); } @override Widget build(BuildContext context) => widget.build(context); }Copy the code

The AnimatedBuilder inherits from the AnimatedWidget, which adds a listener by calling the addListener() method, and the Model inherits from the Listenable class. When we call notifyListeners(), we cause _handleChange() in the AnimatedBuilder to execute, and then we call setState() to rebuild. This is why you need to call notifyListeners() after you change the value.

InheritedWidget

The second parameter is a _InheritedModel AnimatedBuilder is inherited from InheritedWidget class, InheritedWidget class can easily let all subcomponents convenient find grandfather elements in the model instance.

class _InheritedModel<T extends Model> extends InheritedWidget { final T model; final int version; _InheritedModel({Key key, Widget child, T model}) : this.model = model, this.version = model._version, super(key: key, child: child); @override bool updateShouldNotify(_InheritedModel<T> oldWidget) => (oldWidget.version ! = version); }Copy the code

Inheritedwidgets can effectively pass and share data in the component tree. InheritedWidget as root widget, child widgets can be inheritFromWidgetOfExactType () method returns the distance from its recent InheritedWidget instance, Also register it in the InheritedWidget, and when the data in the InheritedWidget changes, the Child Widget is rebuilt as well.

When InheritedWidget is rebuilt, the updateShouldNotify() method is called to determine whether to rebuild the Child Widget.

When we call the Model notifyListeners() method, the version increments, and then the InheritedWidget uses the version to determine if the Child Widget needs to be notified of the update.

One thing to note is that the AnimatedBuilder will re-render the values returned by its Builder if it executes notifyListeners(), but if we are careful enough we can see that its children are not re-rendered (in the case of the MaterialApp). This is because the MaterialApp is passed as a parameter to the ScopedModel, and the ScopedModel caged it with a Child variable, so the MaterialApp is not re-rendered when setState is executed.

conclusion

ScopedModel uses AnimatedBuilder and InheritedWidget to manage its state. Providers and Redux use an InheritedWidget in a similar way. The only change is the use of subscription notifications. For example, Redux is implemented using a Stream. That is my understanding of the ScopedModel welcome to discuss, if I have something wrong feel free to comment.