The ScopedModel has overstepped into Provider mode. Without going into this article, we can see that the VM layer in ScopedMode is notified of interface updates by calling the notifyListeners method. ScopedModel and ScopedModelDescendant are similar to the Consumer in Provider mode, but are essentially one component. It is also used at a level in the subtree of components that need to be updated to keep the update scope to a minimum. The organization of the VM is basically the same, using the VM layer to invoke various services. So, if you already know the Provider pattern, you can skip this video. If you are not familiar with providers, you can skip this article and look at the Provider pattern.

This paper hopes to clearly explain how to use ScopedModel architecture under the condition of being as close to actual practice as possible. Video tutorials are available here.

The cause of

I was helping a client recreate an App with Flutter. The design is poor and the performance is ridiculously poor. But I only used Flutter for three weeks when I took over the project. ScopedMode and Redux were investigated and ScopedModel was used. BLoC was completely out of the picture.

I found ScopedModel very easy to use, and I learned a lot from developing this app.

Implementation style

There is more than one implementation of the ScopedModel. Organize models by functionality, or by pages. In both cases, the model needs to interact with the service, which handles all the logic and handles the state based on the returned data. Let’s go through both of these very quickly.

An AppModel and FeatureModel mixin

In this case you have an AppModel that passes from the root widget all the way to the required child components. AppModel can extend its support through mixins such as:

/// Feature model for authentication
class AuthModel extends Model {
    // ...
}

/// App model
class AppModel extends Model with AuthModel {}
Copy the code

If you still don’t know what’s going on, look at this example.

One Model per page or component

A ScopedModel is then associated directly with a page or component. But it also generates a lot of schema code, because you have to write a Model for each page.

On the production app, a single AppModel and multiple functional mixins are used. As apps get bigger, it’s often the case that one model handles the state of multiple pages (components), which can get a little frustrating. So they moved to a different way of doing things. One Model per page/component, with GetIt as an IoC container, makes it much easier. This pattern will continue for the rest of this article.

You can start with code in the REPO if you want to get your hands dirty. Open the start directory with your favorite IDE.

Implementation summary

The idea is to make it easier to get started and find an entry point. Each view will have a root Model that inherits from the self-scoped Model. The ScopedModel object will be retrieved from the locator. It’s called a locator because it’s used to locate services and models. Each page/component’s Model proxys specific service methods, such as network requests or database operations, and updates the component’s state based on the returned results.

First, let’s install GetIt and ScopedModel.

implementation

Configure and install ScopedModel and dependency injection

Add the scoped_model and get_IT dependencies to our package manifest pubSpec:

.
dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^ 0.1.2
  # scoped model
  scoped_model: ^ 1.0.1
  # dependency injection
  get_it: ^ 1.0.3
.
Copy the code

Create a service_locator. Dart file in the lib directory. Add the following code:

import 'package:get_it/get_it.dart';

GetIt locator = GetIt();

void setupLocator() {
  // Register services

  // Register models
}
Copy the code

This is where you register all your Models and service objects. Dart then adds a call to setupLocator() as follows:

.import 'service_locator.dart';

void main() {
  // setup locatorsetupLocator(); runApp(MyApp()); }...Copy the code

That’s all you need to configure your app’s dependencies.

Add components and models

Let’s add a Home page. Now that each page has a Scoped Model, create a new one and link the two by locator. First we prepare the place where they will be stored. Create a UI directory in the lib directory and create a View directory to hold the split view.

Create a new directory in the lib directory called scoped_model to hold the model.

Dart file in the view directory.

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';

class HomeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnScopedModel<HomeModel>( child: Scaffold( )); }}Copy the code

We need a HomeModel to get all the corresponding information we need. Dart file in the lib/scoped_model directory.

import 'package:scoped_model/scoped_model.dart';

class HomeModel extends Model {}Copy the code

Next we need to associate our page with the Scoped Model. This is where the aforementioned locator comes in. However, there are still some locator registrations to complete. Registration is required to apply a locator.

import 'package:scoped_guide/scoped_models/home_model.dart'; .void setupLocator() {
  // register services
  // register models
  locator.registerFactory<HomeModel>(() => HomeModel());
}
Copy the code

HomeModel has been registered in a locator. We can now get an instance of it from a locator anywhere.

First we need to introduce the ScopedModel, which uses generics, so its type parameter is the HomeModel we defined. Put it in the build method as a component. The Model property uses a locator. Use ScopedModelDescendant where HomeModel instances are used. It also needs a type parameter, again HomeModel.

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/scoped_models/home_model.dart';
import 'package:scoped_guide/service_locator.dart';

class HomeView extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnScopedModel<HomeModel>( model: locator<HomeModel>(), child: ScopedModelDescendant<HomeModel>( builder: (context, child, model) => Scaffold( body: Center( child: Text(model.title), ), ))); }}Copy the code

The title property of the model here can be set to HomeModel.

Add the service

Create a new lib/services directory. Here we will add a fake service that will only delay execution by two seconds and then return true. Add a storage_service.dart file.

class StorageService {
  Future<bool> saveData() async {
    await Future.delayed(Duration(seconds: 2));
    return true; }}Copy the code

Register this service with a locator:

import 'package:scoped_guide/services/storage_service.dart'; .void setupLocator() {
  // register services
  locator.registerLazySingleton<StorageService>(() => StorageService());

  // register models
  locator.registerFactory<HomeModel>(() => HomeModel());
}
Copy the code

As mentioned above, we use service to do the required work and use the returned data to update the components that need to be updated. However, there is also a Model as a proxy. So we need a locator to associate the registered service with the Model.

import 'package:scoped_guide/service_locator.dart';
import 'package:scoped_guide/services/storage_service.dart';
import 'package:scoped_model/scoped_model.dart';

class HomeModel extends Model {
  StorageService storageService = locator<StorageService>();

  String title = "HomeModel";

  Future saveData() async {
    setTitle("Saving Data");
    await storageService.saveData();
    setTitle("Data Saved");
  }

  void setTitle(Stringvalue) { title = value; notifyListeners(); }}Copy the code

The saveData method in HomeModel is what the component needs to call. This approach is also a powerful approach to service. Specific reference can be made to the MVVM model, here will not be described.

In the saveData method, the setTitle method is called after the data is saved. The method sets the title property based on the value returned by the service and calls the notifyListeners method to notify it. Notify the component that needs to be updated that the data can be displayed.

Add a float button to the Scaffold of HomeView and call the saveData method of HomeModel inside. Then, from receiving user input to “save data”, and finally update the interface a set of processes in the code is complete.

Review the basics

Let’s review some of the things that are often used in real development.

State management

As your app accesses data from the network or a local database, the spectrum of data is retrieved and processed in four basic states: IDEL, BUSY, Retrieved data, and Error. All views use these four states, so it’s a good idea to write them to the Model at the beginning.

Create a new lib/enum directory and create a new view_states.dart file.

/// Represents a view's state from the ScopedModel
enum ViewState {
  Idle,
  Busy,
  Retrieved,
  Error
}
Copy the code

Now the view’s model can introduce ViewState.

import 'package:scoped_guide/service_locator.dart';
import 'package:scoped_guide/services/storage_service.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/enums/view_state.dart';

class HomeModel extends Model {
  StorageService storageService = locator<StorageService>();

  String title = "HomeModel";

  ViewState _state;
  ViewState get state => _state;

  Future saveData() async {
    _setState(ViewState.Busy);
    title = "Saving Data";
    await storageService.saveData();
    title = "Data Saved";
    _setState(ViewState.Retrieved);
  }

  void_setState(ViewState newState) { _state = newState; notifyListeners(); }}Copy the code

ViewState is exposed through a getter. Again, these states need to be captured by the corresponding view and updated when changes occur. Therefore, notifyListeners are called to notify the view of changes in state, or update the state of the view.

As you can see, a method called _setState is called when the state changes. This method is specifically responsible for calling notifyListeners to notify the view of updates.

Now that we call _setState, the ScopedModel will be notified and some part of the UI will change. We will display a rotating chrysanthemum to indicate that the service is requesting data, either over the network for back-end data or from a local database. Now update the Scaffold code:

. body: Center( child: Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ _getBodyUi(model.state), Text(model.title), ] ) ) ... Widget _getBodyUi(ViewState state) {switch (state) {
    case ViewState.Busy:
      return CircularProgressIndicator();
    case ViewState.Retrieved:
    default:
      return Text('Done'); }}Copy the code

The _getBodyUi method has a ViewState value to display different interfaces.

Multiple views

Changes to one piece of data that affect multiple interfaces are a common occurrence in real-world development. After dealing with the simple case of a single interface update we can start dealing with multiple interfaces.

In the previous examples you saw a lot of template code, such as ScopedModel, ScopedModelDescendant, and retrieving objects like Model and Service from a locator. This is all template code, not much, but we can make it less.

First, let’s create a new BaseView.

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/service_locator.dart';

class BaseView<T extends Model> extends StatelessWidget {
  
  final ScopedModelDescendantBuilder<T> _builder;

  BaseView({ScopedModelDescendantBuilder<T> builder})
      : _builder = builder;

  @override
  Widget build(BuildContext context) {
    returnScopedModel<T>( model: locator<T>(), child: ScopedModelDescendant<T>( builder: _builder)); }}Copy the code

There are already calls to ScopedModel and ScopedModelDescendant in BaseView. So don’t put these calls in every interface. HomeView, for example, uses BaseView and gets rid of all this code.

.import 'base_view.dart';

@override
Widget build(BuildContext context) {
  returnBaseView<HomeModel> ( builder: (context, child, model) => Scaffold( ... ) ); }Copy the code

So we can do more with less code. You can register a code snippet in the IDE, so that a few characters can be typed and a nearly complete functional code appears. Dart is a new template file, template_view.dart, in lib/ UI /views.

import 'package:flutter/material.dart';
import 'package:scoped_guide/scoped_models/home_model.dart';

import 'base_view.dart';

class Template extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseView<HomeModel>(
      builder: (context, child, model) => Scaffold(
         body: Center(child: Text(this.runtimeType.toString()),), )); }}Copy the code

The state we distribute is not unique to one interface, but can be shared by multiple interfaces, so we also create a new BaseModel to handle this problem.

import 'package:scoped_guide/enums/view_state.dart';
import 'package:scoped_model/scoped_model.dart';

class BaseModel extends Model {
  ViewState _state;
  ViewState get state => _state;

  voidsetState(ViewState newState) { _state = newState; notifyListeners(); }}Copy the code

Modify HomeModel’s code to inherit from BaseModel.

.class HomeModel extends BaseModel {... Future saveData()async {
    setState(ViewState.Busy);
    title = "Saving Data";
    await storageService.saveData();
    title = "Data Saved"; setState(ViewState.Retrieved); }}Copy the code

The code is ready to support multiple interfaces. We now have BaseView and BaseModel serving the view and model, respectively.

The next step is navigation. Dart creates two new views, error_view.dart and Success_view. dart, based on template_view.dart. Remember to make appropriate changes in the code.

Next, create two new Models, one SuccessModel and one ErrorModel. They all inherit from BaseModel, not Model. Then remember to register the models in the locator.

navigation

The basic navigation is very similar. We can use the Navigator to initialize the view on the stack.

Now let’s make some changes to our home mode #saveData.

Future<bool> saveData() async {
    _setState(ViewState.Busy);
    title = "Saving Data";
    await storageService.saveData();
    title = "Data Saved";
    _setState(ViewState.Retrieved);
    
    return true;
}
Copy the code

In HomeView, let’s update the float button’s onPress method. Make it an asynchronous method, wait for the result of saveData execution, and navigate to the appropriate interface based on the result.

floatingActionButton: FloatingActionButton(
    onPressed: () async {
      var whereToNavigate = await model.saveData();
      if (whereToNavigate) {
        Navigator.push(context,MaterialPageRoute(builder: (context) => SuccessView()));
      } else{ Navigator.push(context,MaterialPageRoute(builder: (context) => ErrorView())); }})Copy the code

Shared views

There are services that fetch data in multiple facets, so they all need to show busy state: a revolving chrysanthemum. This component, then, can be shared between different interfaces.

Create a new BusyOverlay component, place it in the lib/ UI /views directory and call it busy_overlay. Dart.

import 'package:flutter/material.dart';

class BusyOverlay extends StatelessWidget {
  final Widget child;
  final String title;
  final bool show;

  const BusyOverlay({this.child,
      this.title = 'Please wait... '.this.show = false});

  @override
  Widget build(BuildContext context) {
    var screenSize = MediaQuery.of(context).size;
    return Material(
        child: Stack(children: <Widget>[
      child,
      IgnorePointer(
        child: Opacity(
            opacity: show ? 1.0 : 0.0,
            child: Container(
              width: screenSize.width,
              height: screenSize.height,
              alignment: Alignment.center,
              color: Color.fromARGB(100.0.0.0),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                children: <Widget>[
                  CircularProgressIndicator(),
                  Text(title,
                      style: TextStyle(
                          fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.white)), ], ), )), ), ])); }}Copy the code

Now we can use this component in the interface. In HomeView, put that Scaffold in BusyOverlay:

@override
Widget build(BuildContext context) {
  return BaseView<HomeModel>(builder: (context, child, model) =>
     BusyOverlay(
      show: model.state == ViewState.Busy, child: Scaffold( ... ) )); }Copy the code

Now, when you click the float button you will see a “please wait” message. You can also put calls to the BusyOverlay component into the BaseView. Remember that your busy prompt group needs to be in the Builder so that it can react correctly to the model return value.

Handling of asynchronous problems

We’ve dealt with displaying the corresponding interface based on the different model return values. Now we’ll deal with another common problem, which is asynchronous problem handling.

Load the page and get the data

When you have a list and you click on a row to see more details you basically have an asynchronous scenario. When entering the details page, we will request the back end to get more details according to the ID and other relevant data of this particular data.

Requests typically occur in the initState method of the StatefulWidget. We’re not going to add too many interfaces in this example, we’re just going to focus on the architecture. We write out a return value that is returned to the interface when the request is successful.

First, let’s update SuccessModel.

import 'package:scoped_guide/scoped_models/base_model.dart';

class SuccessModel extends BaseModel {
  String title = "no text yet";

  Future fetchDuplicatedText(String text) async {
    setState(ViewState.Busy);
    await Future.delayed(Duration(seconds: 2));
    title = '$text $text'; setState(ViewState.Retrieved); }}Copy the code

Now we can call model’s methods at view creation time. However, this requires us to replace the BaseView with a StatefulWidget. Call the asynchronous method of Model in the initState method of BaseView.

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_guide/service_locator.dart';

class BaseView<T extends Model> extends StatefulWidget {
  final ScopedModelDescendantBuilder<T> _builder;
  final Function(T) onModelReady;

  BaseView({ScopedModelDescendantBuilder<T> builder, this.onModelReady})
      : _builder = builder;

  @override
  _BaseViewState<T> createState() => _BaseViewState<T>();
}

class _BaseViewState<T extends Model> extends State<BaseView<T>> {
  T _model = locator<T>();

  @override
  void initState() {
    if(widget.onModelReady ! =null) {
      widget.onModelReady(_model);
    }
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    returnScopedModel<T>( model: _model, child: ScopedModelDescendant<T>( child: Container(color: Colors.red), builder: widget._builder)); }}Copy the code

Then update your SuccessView and pass in the method you want to call in the onMondelReady property.

class SuccessView extends StatelessWidget {
  final String title;

  SuccessView({this.title});

  @override
  Widget build(BuildContext context) {
    return BaseView<SuccessModel>(
        onModelReady: (model) => model.fetchDuplicatedText(title),
        builder: (context, child, model) => BusyOverlay(
            show: model.state == ViewState.Busy, child: Scaffold( body: Center(child: Text(model.title)), ))); }}Copy the code

Finally, the parameters are passed in during navigation.

Navigator.push(context, MaterialPageRoute(builder: (context) = > SuccessView(title: 'Pass in from home')));
Copy the code

That will do. Now you can run your app under ScopedModel.

complete

This article basically covers everything you need to develop an app using ScopedModel. At this point you are ready to implement your own service. An important issue not covered in this article is testing.

We can also implement dependency injection through constructors, such as injecting services into the Model through dependency injection of constructors. This way we can also inject some fake services. I didn’t test the Model layer because they were completely dependent on the service layer. And I’ve done a lot of testing on the service layer.