0

I have scoped model lib/scoped_models/main.dart:

import 'package:scoped_model/scoped_model.dart';

class MainModel extends Model {
  int _count = 0;

  int get count {
    return _count;
  }
  
  void incrementCount() {
    _count += 1;
    notifyListeners();
  }

  void setCount(int value) {
    _count = value;
    notifyListeners();
}

And very simple app lib/main.dart:

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_m_test/scoped_models/main.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<MainModel>(
        model: MainModel(),
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        )
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final MainModel _model = MainModel();

  void initState() {
    super.initState();
    // _model.incrementCount(); // <-- doesn't work !!!
  }
  
  void _incrementCounter() {
    setState(() {
      // _model.incrementCount(); // <-- doesn't work !!!
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            ScopedModelDescendant<MainModel>(
              builder: (BuildContext context, Widget child, MainModel model) {
                return Text(
                  '${model.count}',
                  style: Theme.of(context).textTheme.headline4,
                );
              }
            )
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<MainModel>(
        builder: (BuildContext context, Widget child, MainModel model) {
          return FloatingActionButton(
            onPressed: () {
              model.incrementCount(); // <-- only this works !!!
              // _incrementCounter(); // <-- doesn't work !!!
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        }
      )
    );
  }
}

The problem that I can't access MainModel outside of ScopedModelDescendant widget.

How to call MainModel methods at the beginning of _MyHomePageState class?

I believe it is possible because I don't want to keep all logic just in MainModel class and call every method in ScopedModelDescendant widget because it would be very inconvenient if there were many nested widgets.

So, how to get access to scoped model in StatefulWidget?

mr.boris
  • 3,667
  • 8
  • 37
  • 70

2 Answers2

1

Use Scoped Model as provider

  • add ScopedModel just before the widget which use it (MyHomePage)
  • use ScopedModel.of<MainModel>(context) to control the model
  • use ScopedModelDescendant<MainModel> to listen the model

The advantage of using this:

  • You can access the same model in the descendants and share data easily
  • rebuild widget as small as possible (only ScopedModelDescendant part will be rebuilt)

code:

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: ScopedModel<MainModel>(
        model: MainModel(),
        child: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  void initState() {
    super.initState();
  }

  void _incrementCounter() {
    ScopedModel.of<MainModel>(context).incrementCount();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            ScopedModelDescendant<MainModel>(
              builder: (context,child, model){
                return Text(
                  '${model.count}',
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _incrementCounter();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Put MainModel as a Singleton

As your solution, you create MainModel once and make it final. This can be more simple like below:

MainModel

final MainModel mainModel = MainModel();

class MainModel{
  int _count = 0;

  int get count {
    return _count;
  }

  void incrementCount() {
    _count += 1;
  }

  void setCount(int value) {
    _count = value;
  }
}

MyHomePage

  • MainModel even no need to extend Model or use notifyListeners becaue the widget use setState to rebuild

code:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  void initState() {
    super.initState();
  }

  void _incrementCounter() {
    setState(() {
      mainModel.incrementCount();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '${mainModel.count}',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          _incrementCounter();
        },
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
yellowgray
  • 4,006
  • 6
  • 28
0

After watching into my code for a while I realized how stupid simple it was to fix.

So, obviously there should be just one instance of MainModel() for all widgets and files of the project and for convenience it should be placed in scoped model file lib/scoped_models/main.dart like this:

import 'package:scoped_model/scoped_model.dart';

final MainModel mainModel = MainModel(); // <-- create instance once for all files which require scoped model import

class MainModel extends Model {
  int _count = 0;

  int get count {
    return _count;
  }
  
  void incrementCount() {
    _count += 1;
    notifyListeners();
  }

  void setCount(int value) {
    _count = value;
    notifyListeners();
}

And then you can use mainModel instance anywhere you import the model import 'package:<app_name>/scoped_models/main.dart';

So that, this code will be valid lib/main.dart:

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_m_test/scoped_models/main.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ScopedModel<MainModel>(
        model: mainModel, // <-- instance of model from 'lib/<app_name>/scoped_models/main.dart'
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: MyHomePage(title: 'Flutter Demo Home Page'),
        )
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  void initState() {
    super.initState();
  }
  
  void _incrementCounter() {
    setState(() {
      mainModel.incrementCount(); // <-- now it works !!!
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            ScopedModelDescendant<MainModel>(
              builder: (BuildContext context, Widget child, MainModel model) {
                return Text(
                  '${model.count}',
                  style: Theme.of(context).textTheme.headline4,
                );
              }
            )
          ],
        ),
      ),
      floatingActionButton: ScopedModelDescendant<MainModel>(
        builder: (BuildContext context, Widget child, MainModel model) {
          return FloatingActionButton(
            onPressed: () {
              // model.incrementCount(); // <-- works !!!
              _incrementCounter(); // <-- now it's working too !!!
            },
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        }
      )
    );
  }
}

Despite that fact that is seems reasonable, it can be overwhelming as well for the first time due to lack of examples.

halfer
  • 19,824
  • 17
  • 99
  • 186
mr.boris
  • 3,667
  • 8
  • 37
  • 70
  • It is a little strange that you make your `mainModel` as a global singleton, why you need `ScopedModel` anymore? You can simply use `class MainModel extends ChangeNotifier` and read/write it anywhere. – yellowgray Jan 14 '21 at 13:50
  • Thanks for comment but I don't know how to implement your approach properly. – mr.boris Jan 14 '21 at 14:54
  • I try to explain it on the other answer. Let me know if you have any further question. – yellowgray Jan 14 '21 at 16:28