Flutter State Management - ScopedModel

I. Introduction

A lot of Flutter's inspiration comes from React, and its design idea is to separate the data from the view, and render the view by the data map . So in Flutter, its Widget is immutable, and all its dynamic parts are placed in the state.

What is Scoped_model

Scoped_model is a dart third-party library that provides functionality that allows you to easily pass a data model from a parent widget to its descendants. Additionally, it re-renders all children that use the model when the model is updated.

It comes directly from a simple extraction of the Model class in Fuchsia core Widgets, a new system under development by Google, released as a standalone Flutter plugin for stand-alone use.

Implementation principle

The Scoped model uses the observer mode, which puts the data model in the parent, and the descendants render the data by finding the parent's model. Finally, when the data changes, the data is sent back, and the parent notifies all the children who use the model to update. condition.

And we need to put them above the top-level entry MaterialApp, so that global state management can be performed.

Here page3 and page4 represent sub-pages that use this state (model).

2. Introduce the ScopedModel third-party library

// pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  scoped_model: ^1.0.1

Failed to add due to version conflict, please refer to: 

Flutter | How to Elegantly Resolve Dependency Version Conflicts - After Nuggets Google launched flutter, a new high-performance cross-platform (Android, ios) rapid development framework, it has attracted the attention of many developers in the industry. After I came into contact with flutter, I found that this is indeed a good thing, and of course good things should be shared with you, right? What I want to share with you today is how to solve the dependency version conflict in flutter. This article is the most… https://juejin.cn/post/6844903667955400718

3. New Model

// CountModel.dart
import 'package:scoped_model/scoped_model.dart';

class CountModel extends Model {
  int _count = 0;
  int get count => _count;

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

4. Partial refresh (single component/single page internal state)

4.1 New page (ScopedModelPage)

Scoped_model provides two ways to get models in subpages.

Let's first introduce the first one, using ScopedModelDescendant to get the model.

// ScopedModelPage.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';

class ScopedModelPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<CountModel>(
      model: CountModel(),
      child: Scaffold(
        appBar: AppBar(
          title: Text("ScopedModelPage"),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('You have pushed the button this many times:'),
              ScopedModelDescendant<CountModel>(
                builder: (context, child, model) => Text('${model.count}'),
              ),
            ],
          ),
        ),
        floatingActionButton: ScopedModelDescendant<CountModel>(
          builder: (context, child, model) {
            return FloatingActionButton(
              onPressed: model.increment,
              tooltip: 'Increment',
              child: Icon(Icons.add),
            );
          },
        ),
      ),
    );
  }
}

ScopedModelDescendant<T extends Model> is a Stateless Widget that receives three parameters.

  ScopedModelDescendant({
    required this.builder,
    this.child,
    this.rebuildOnChange = true,
  });

builder is a ScopedModelDescendantBuilder that takes three parameters.

typedef Widget ScopedModelDescendantBuilder<T extends Model>(
  BuildContext context,
  Widget? child,
  T model,
);

The CountModel instance can be obtained through the model in the builder. 

The rebuildOnChange property can control whether to rebuild when the state changes, which is equivalent to setState. That is to say, when we call some methods that change the state, we don't have to setState.

The second way to get the model - use ScopedModel.of

final countModel = ScopedModel.of<CountModel>(context);
countModel.increment();

Or override the of method in Model

class CountModel extends Model{
  int _count = 0;
  get count => _count;

  void increment(){
    _count++;
    notifyListeners();
  }
//重写of方法
  CountModel of(context) =>
      ScopedModel.of<CountModel>(context);
}

Then get the model instance directly through CountModel

final countModel2 = CountModel().of(context);

This approach seems to make our code more readable.

【Notice:】

When we use the second method, the value of rebuildOnChange is changed to false, which will cause the situation that the state cannot be refreshed (synchronized). It is necessary to specify rebuildOnChange: true by default. Usually, there is no need to manually specify the value of rebuildOnChange for development.

4.2, modify the main file

// 改写 main.dart
import 'package:flutter/material.dart';
import 'package:stateresearch/pages/ScopedModelPage.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter状态管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModelPage(),
    );
  }
}

5. Global refresh (page/component state sharing)

5.1. Two new pages (ScopedModelPageTwo and ScopedModelPageThree) are added

// ScopedModelPageTwo.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';
import 'package:stateresearch/pages/ScopedModelPageThree.dart';

class ScopedModelPageTwo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageTwo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<CountModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: () {
              model.increment();
              Future.delayed(Duration(seconds: 2), () {
                Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
                  return ScopedModelPageThree();
                }));
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}
// ScopedModelPageThree.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';

class ScopedModelPageThree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageThree"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<CountModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<CountModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: model.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

5.2, modify the main file

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';
import 'package:stateresearch/pages/ScopedModelPageTwo.dart';

void main() {
  runApp(
     // APP顶层进行全局监听
     // route 会进行向下传递该 Model
    // 因此其它页面无需 ScopedModel
    // 只需要通过 ScopedModelDescendant<T> 获取 Model 即可
    ScopedModel<CountModel>(
        model: CountModel(),
        child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter状态管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModelPageTwo(),
    );
  }
}

6. Global sharing of multiple models

6.1. Added Model (ListModel) and mixin's Model (GlobalScopedModel)

// ListModel.dart
import 'package:scoped_model/scoped_model.dart';

class ListModel extends Model {
  List<String> _list = [];
  List<String> get list => _list;

  void push(String value) {
    _list.add(value);
    notifyListeners();
  }
}
// GlobalScopedModel.dart
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/CountModel.dart';
import 'package:stateresearch/model/ListModel.dart';

class GlobalScopedModel extends Model with CountModel, ListModel {}

6.2. Added analysis configuration

// analysis_options.yaml
// 该配置告诉Dart Analyzer放开minx的限制
// 默认with的类强制是继承于Object类
analyzer:
  errors:
    mixin_inherits_from_not_object: ignore

6.3. Modify two pages (ScopedModelPageTwo and ScopedModelPageThree)

// ScopedModelPageTwo.dart
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/GlobalScopedModel.dart';
import 'package:stateresearch/pages/ScopedModelPageThree.dart';

class ScopedModelPageTwo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageTwo"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<GlobalScopedModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<GlobalScopedModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: () {
              model.increment();
              model.push("chris-${Random().nextInt(10)}");
              Future.delayed(Duration(seconds: 2), () {
                Navigator.of(context).push(MaterialPageRoute(builder: (BuildContext context) {
                  return ScopedModelPageThree();
                }));
              });
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}
// ScopedModelPageThree.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/GlobalScopedModel.dart';

class ScopedModelPageThree extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("ScopedModelPageThree"),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<GlobalScopedModel>(
              builder: (context, child, model) => Text('${model.count}'),
            ),
            ScopedModelDescendant<GlobalScopedModel>(
              builder: (context, child, model) => Text('${model.list}'),
            ),
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<GlobalScopedModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: model.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}

6.4, modify the main file

// main.dart
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:stateresearch/model/GlobalScopedModel.dart';
import 'package:stateresearch/pages/ScopedModelPageTwo.dart';

void main() {
  runApp(
    ScopedModel<GlobalScopedModel>(
      model: GlobalScopedModel(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter状态管理',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModelPageTwo(),
    );
  }
}

7. Summary

ScopedModel can be used globally + locally (even if the global ScopedModel is used, it does not affect a Widget using its own ScopedModel)

Using ScopedModel, its advantages:

  • Display logic and business logic are separated;

shortcoming:

  • When the model is complex, the timing of notifyListeners is very important, otherwise it will be refreshed frequently;
  • The API of the Model is asynchronous (Microtask) internally, but its API name cannot be seen as asynchronous;

extend

Model of source code

abstract class Model extends Listenable {
  final Set<VoidCallback> _listeners = Set<VoidCallback>();
  int _version = 0;
  int _microtaskVersion = 0;

  /// [listener] will be invoked when the model changes.
  @override
  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  /// [listener] will no longer be invoked when the model changes.
  @override
  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }

  /// Returns the number of listeners listening to this model.
  int get listenerCount => _listeners.length;

  /// Should be called only by [Model] when the model has changed.
  @protected
  void notifyListeners() {
    // We schedule a microtask to debounce multiple changes that can occur
    // all at once.
    if (_microtaskVersion == _version) {
      _microtaskVersion++;
      scheduleMicrotask(() {
        _version++;
        _microtaskVersion = _version;

        // Convert the Set to a List before executing each listener. This
        // prevents errors that can arise if a listener removes itself during
        // invocation!
        _listeners.toList().forEach((VoidCallback listener) => listener());
      });
    }
  }
}
  • ScopedModel

注意深追 AnimatedBuilder --> AnimatedWidget
_InheritedModel --> InheritedWidget

class ScopedModel<T extends Model> extends StatelessWidget {
  /// The [Model] to provide to [child] and its descendants.
  final T model;

  /// The [Widget] the [model] will be available to.
  final Widget child;

  ScopedModel({@required this.model, @required this.child})
      : assert(model != null),
        assert(child != null);

  @override
  Widget build(BuildContext context) {
    // 注意深追AnimatedBuilder,--> AnimatedWidget  
    return AnimatedBuilder(
      animation: model,
      builder: (context, _) => _InheritedModel<T>(model: model, child: child),
    );
  }
}
  • In the state of AnimatedWidget

The listenable here is to addListener in the previous model initState, and the parameter method is setState

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);

  final Listenable listenable;

  @override
  _AnimatedState createState() => _AnimatedState();

}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }
  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

}

  • _InheritedModel

This is an InheritedWidget, the previous model and the Widget that gets data through the model are finally passed here

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);
}

ScopedModelDescendant is a layer of encapsulation to obtain the shared model. The model is obtained through ScopedModel.of(context, rebuildOnChange: rebuildOnChange), and then passed through the builder. The type of build is ScopedModelDescendantBuilder

class ScopedModelDescendant<T extends Model> extends StatelessWidget {
 
  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      child,
      ScopedModel.of<T>(context, rebuildOnChange: rebuildOnChange),
    );
  }
}

typedef Widget ScopedModelDescendantBuilder<T extends Model>(
  BuildContext context,
  Widget child,
  T model,
);

How does Scoped synchronize the state of different pages

As can be seen from the above source code, the Model implements the Listenable interface, and rewrites the methods of void addListener(VoidCallback listener) and removeListener(VoidCallback listener) to implement the observer mode. Whenever we call the notifyListeners() method, the observers will be notified to update the state.

How Scoped can share data with each other

InheritedWidget is used for data transfer between different pages.

class _InheritedModel<T extends Model> extends InheritedWidget {
  final T model;
  final int version;

  _InheritedModel({Key? key, required Widget child, required T model})
      : this.model = model,
        this.version = model._version,
        super(key: key, child: child);

  @override
  bool updateShouldNotify(_InheritedModel<T> oldWidget) =>
      (oldWidget.version != version);
}

invasive

Since Model must inherit from the Model class, it is intrusive. In the future, if you do not use scoped for state management, you will inevitably need to change multiple codes. This is not the result we hoped to see.

Reference article:

Flutter State Management: ScopedModel - Programmer Sought

Guess you like

Origin blog.csdn.net/jdsjlzx/article/details/123263160