Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to connect multiple models in connector (ScopedModel) #9

Closed
chimon2000 opened this issue Apr 16, 2018 · 31 comments
Closed

Ability to connect multiple models in connector (ScopedModel) #9

chimon2000 opened this issue Apr 16, 2018 · 31 comments

Comments

@chimon2000
Copy link

chimon2000 commented Apr 16, 2018

I am a big fan of unstated which takes advantages of React's new Context API and shares many similarities to this library.

It would be nice if you could combine multiple models in the subscriber

new ScopedModel(
      model: [new BookModel(), new CounterModel()],
      child: new Column(children: [
        new ScopedModelDescendant(
                builder: (context, child, { bookModel, counterModel }) => new Text(
                    counterModel.counter.toString()),
              ),
        new Text("Another widget that doesn't depend on the CounterModel")
      ])

similar to how you can do it in unstated

<Subscribe to={[BookContainer, CounterContainer]}>
  {
    (bookStore, counterStore) => {
      // do stuff here
    }
  }
</Subscribe>
@brianegan
Copy link
Owner

brianegan commented Apr 16, 2018

Thanks! Definitely an interesting idea. I'm a bit swamped right now and can't guarantee I'll be able to get this done in the next couple weeks, but I'll take a look at it and see how we can do it in a sane way.

In order to support type safety, we'll need to carefully think about the API. For example, we could allow the user to provide a List<Model<dynamic>> as you've suggested, but then you'd lose all type information about which model you're working with.

Therefore, we might need something like:

new ScopedModel2<BookModel, CounterModel>(
  models: [new BookModel(), new CounterModel()],
  child: ...
);
new ScopedModel3<BookModel, CounterModel>(
  models: [new BookModel(), new CounterModel(), new SearchModel()],
  child: ...
);

As well as corresponding ScopedModelDescendant2, ScopedModelDescendant3, etc

@chimon2000
Copy link
Author

chimon2000 commented Apr 16, 2018

Definitely a fan of the type safe approach. Is it possible to have a class that accepts an infinite number of types in Dart? I was unsure about it. The other piece that I was unsure of is how those items would be destructured in the builder.

After digging around Dart's documentation, I did find a pattern using mixins that seems equally robust w/o changing the api:

class CounterModel extends Model {
  var _counter = 0;

  int get counter => _counter;
}

abstract class IncrementModel extends CounterModel {
  void increment() {
    // First, increment the counter
    _counter++;

    // Then notify all the listeners.
    notifyListeners();
  }
}

abstract class DecrementModel extends CounterModel {
  void decrement() {
    // First, decrement the counter
    _counter--;

    // Then notify all the listeners.
    notifyListeners();
  }
}

abstract class ResetModel extends CounterModel {
  void reset() {
    // First, reset the counter
    _counter = 0;

    // Then notify all the listeners.
    notifyListeners();
  }
}

class MainModel extends Model
    with CounterModel, IncrementModel, DecrementModel, ResetModel {}

This is a contrived example but it simulates how you can compose an infinite number of models together (similar to combineReducers in redux).

@brianegan
Copy link
Owner

brianegan commented Apr 17, 2018

Yah, that's an interesting approach. Just saw your blog post -- as an example without this context, this might a bit hard to follow (e.g. I first thought: Why doesn't he just put all of those methods together to form one logical model?). It might interesting to compose different types of State together, such as a UserState and a SearchState as an example to show the benefits of the approach.

@chimon2000
Copy link
Author

Yes, I think that is a much better example that demonstrates the value. #laziness. I will probably update my post to include some context.

@roeierez
Copy link

Hi,
I have followed this discussion and one drawback about the pattern is that from the widget tree perspective it is still one big Model. This means that notification changes will render every widget that uses this Model even if the widget is only interested in one "Sub Model", for example "UserState".
Although I think it is very elegant separation.
Do you think I am wrong here?

@brianegan
Copy link
Owner

brianegan commented May 12, 2018

@roeierez Yep, that's definitely true. I think this functionality might be useful in certain cases where you might want to combine different models together that are related for a specific part of the Widget tree without having to create 2-3 ScopedModels and corresponding ScopedModelDescendants to help reduce the amount of code ya need to write.

@gcshri
Copy link

gcshri commented Jun 23, 2018

One of the problems I've been struggling with is passing multiple but different sets of data/models down the widget tree.

In the simplest use case, I'd like to keep track of say AppConfig (a few app related items) and UserConfig (user data, permissions etc). I could make a larger AppState which includes both App, User and other classes, but for sake of keeping code isolated, would prefer separate models which I could pass down.

@brianegan
Copy link
Owner

brianegan commented Aug 17, 2018

Hey all, was just updating scoped model a bit and then it hit me: Listening to multiple models is actually pretty simple to do already. Honestly a bit embarrassed I didn't think of this earlier...

In order to get one Widget to build with two models, you can just use the ScopedModel.of method directly (note: now that Dart 2 supports generics on methods, I'm moving this lib over to a static ScopedModel.of<MyModel> method). The cool thing is: This will even trigger rebuilds of the Widget when either model changes due to the magic of InheritedWidgets!

This still requires multiple ScopedModel Widgets, but providing a list of Widgets might get ugly in some ways. I'm also thinking of splitting the of function up into two different functions for readability: of and watch / listen.

class CombinedWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final username =
      ScopedModel.of<UserModel>(context, rebuildOnChange: true).username;
    final counter =
      ScopedModel.of<CounterModel>(context, rebuildOnChange: true).counter;

    return Text('$username tapped the button $counter times');
  }
}

@brianegan
Copy link
Owner

Added an example here: Let me know if ya think this solves the issue!

https://github.com/brianegan/scoped_model/blob/multiple-models/example/lib/main.dart

@gcshri
Copy link

gcshri commented Aug 17, 2018

@brianegan - thank you. Explains a lot of concepts nicely! Would work very well in my situation where we need about a couple of functional models - appglobals, userglobals and then some more on individual views.

@jonahfang
Copy link

jonahfang commented Aug 19, 2018

I changed the CounterHome widget in https://github.com/brianegan/scoped_model/blob/multiple-models/example/lib/main.dart as follows:

class CounterHome extends StatelessWidget {
  final String title;

  CounterHome(this.title);

  @override
  Widget build(BuildContext context) {
    final userModel = ScopedModel.of<UserModel>(context, rebuildOnChange: true);
    final counterModel =
        ScopedModel.of<CounterModel>(context, rebuildOnChange: true);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('${userModel.username} pushed the button this many times:'),
            Text('${counterModel.counter}',
                style: Theme.of(context).textTheme.display1),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                child: Text('Change Username'),
                onPressed: () {
                  userModel.username = 'Suzanne';
                },
              ),
            )
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => counterModel.increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

Why can we have effect of ScopedModelDescendant but use ScopedModel.of only? Is it necessary to use ScopedModelDescendant again?

@brianegan
Copy link
Owner

brianegan commented Aug 19, 2018

Hey hey :)

Why can we have effect of ScopedModelDescendant but use ScopedModel.of only? Is it necessary to use ScopedModelDescendant again?

This is an interesting implementation detail! The ScopedModel class listens to the Model and builds an InheritedWidget under the hood. Every time the Model calls notifyListeners(), the InheritedWidget will be rebuilt.

InheritedWidgets are pretty cool -- they allow you to pass data down your Widget tree easily, and they also have a hidden superpower:

Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.

Therefore, every time the model changes, it will build a new InheritedWidget, which will then tell every Widget that is connected to it via the of method to rebuild as well if you use the context.inheritFromWidgetOfExactType method!

The only advantage of the ScopedModelDescendant class is that it allows you to use the Model directly in the child of the ScopedModel and subscribe to updates, or if you wan to limit the updates to a specific part of the build method. After that, the two are almost equivalent.

More info:

@jonahfang
Copy link

jonahfang commented Aug 19, 2018

@brianegan Thanks for your detailed explain. I suggest add on method:

final userModel = ScopedModel.of<UserModel>(context, rebuildOnChange: true);
==>
final userModel = ScopedModel.on<UserModel>(context);

@brianegan
Copy link
Owner

Thanks @jonahfang! Yah, was thinking the same thing, but wasn't sure if on and of were too similar (easy to misread, basically). That said, it's nice and short :)

@brianegan
Copy link
Owner

Are folks using this successfully? Does it solve the problem and feel like the issue can be closed?

@gcshri
Copy link

gcshri commented Aug 25, 2018

Hi Brian, I have adapted my code to work based on your multiple model sample. Works as advertised!

Need to look at using the .of(context) in my fluro based route handlers to implement client side authorisation. Have no reason to think it would not work.

@brianegan
Copy link
Owner

Sounds good! I'll go ahead and close this out for now. Please let me know if you run into trouble, or spot any room for improvement in the API :)

@gimox
Copy link

gimox commented Oct 5, 2018

love scope_model approach, and yes , multiple model work perfect but with many models the init code it's a bit confused...

@kk21
Copy link

kk21 commented Oct 7, 2018

The only advantage of the ScopedModelDescendant class is that it allows you to use the Model directly in the child of the ScopedModel and subscribe to updates,

Hi,
I am new and dont understand the 1st advantage mentioned in the above line, possible to explain a bit more, thanks!

@tungvn
Copy link

tungvn commented Oct 31, 2018

The only advantage of the ScopedModelDescendant class is that it allows you to use the Model directly in the child of the ScopedModel and subscribe to updates,

Hi,
I am new and dont understand the 1st advantage mentioned in the above line, possible to explain a bit more, thanks!

As my understanding, I think when you use the ScopedModelDescendant, you can only subscribe to least element-tree you really want to updates when the model changed. In another side, when you use ScopedModel.of, you must call it in the top of Widget.build, and when the model changed, the widget will re-build. It is expensive for the performance.

@Ali-Azmoud
Copy link

Widget build(BuildContext context) {
    // At the top level of our app, we'll, create a ScopedModel Widget. This
    // will provide the CounterModel to all children in the app that request it
    // using a ScopedModelDescendant.
    return ScopedModel<UserModel>( // <========
      model: userModel,
      child: ScopedModel<CounterModel>( // <========
        model: counterModel,
        child: MaterialApp(
          title: 'Scoped Model Demo',
          home: CounterHome('Scoped Model Demo'),
        ),
      ),
    );
  }
}

in your multi model sample, you nested models . since your sample has only 2 models its ok, what if the app grows and at the end there be 10 models or more. should all those 10 models be nested inside each other ?

@pedromassango
Copy link

Widget build(BuildContext context) {
    // At the top level of our app, we'll, create a ScopedModel Widget. This
    // will provide the CounterModel to all children in the app that request it
    // using a ScopedModelDescendant.
    return ScopedModel<UserModel>( // <========
      model: userModel,
      child: ScopedModel<CounterModel>( // <========
        model: counterModel,
        child: MaterialApp(
          title: 'Scoped Model Demo',
          home: CounterHome('Scoped Model Demo'),
        ),
      ),
    );
  }
}

in your multi model sample, you nested models . since your sample has only 2 models its ok, what if the app grows and at the end there be 10 models or more. should all those 10 models be nested inside each other ?

I this way too. My app have five models, do I need to nest inside each other? There is a best way to do that?

@brianegan
Copy link
Owner

brianegan commented Dec 9, 2018

Hey hey -- Just a quick question: Do all of your models contain "Global" data that should be accessed anywhere? If so, then you'd need to currently nest your MaterialApp inside of all of them.

If you only need to access some of these models on specific pages, you could nest a few "Global" models at the top level, and only instantiate new models when you navigate to the Routes that require them.

Overall, I think it's smart to think about where you need data from a Scoped Model, and only provided that Data from that point down. However, if you really have 10 Models that need to be shared globally across your app, then we might need to think about reasonable alternatives to the current implementation.

Unfortunately, the implementation details get tricky here -- if you try to avoid nesting, you often sacrifice some helpful functionality or make other things more complex.

@pedromassango
Copy link

What the advantage of Scoped_Model when comparing with Redux and BLoC?

@brianegan
Copy link
Owner

All of these technologies can be used to separate View logic from Business Logic, they just differ in how they solve that problem.

tl;dr: In my view, scoped_model is the simplest of the three.

While BLoC & Redux are great, they are more complex than scoped_model. That often means you'll be writing a bit more code and need to understand a few more concepts to work with those patterns.

The extra concepts in BLoC are Streams. Streams are really powerful, but take some time to learn. That extra power allows you to do some really neat things that you can't do with Models or Listenables, but the additional complexity might not always be worth it!

I'd argue the same is true about Redux. It takes a bit longer to understand than scoped_model, but can give you some really nice benefits, such as being able Replay the actions a user took through your app. This is immediately useful for crash reporting and measuring user flows through your app.

Hope that helps!

@pedromassango
Copy link

@brianegan My boss give to me a project that is an app like Gmail. Suppose that you are building an Gmail app wich is the best approach: Redux, BLoC or ScopedModel?

I just stopped the development after reaching this question, I do not want to build without a pattern and some time later have to refactor the whole project.

Please give me some tips, it's always good to hear from experts. Thank you in advanced

@gimox
Copy link

gimox commented Dec 9, 2018

@pedromassango I recently build a navigation app, with complex data relation between widgets. I used scope model. I really love this pattern. Very simple and fast for refactoring. Before start my app, i try bloc pattern but, they need a redux to mantain the state, so the alternave to scope can be bloc pattern with redux. I m a angular developer and redux are a standard in complex app. They are very efficient and cover all your needs....but they are complex to mantain and, very hard for refactoring. Scope model are powerful like redux and simple to mantain. My advice is to take time to project your app life cycle, and be careful to use it in the right way, reloading only the widget you need to refresh.

@tungvn
Copy link

tungvn commented Dec 10, 2018

@gimox Me too.
I am a react-redux developer, but on the mobile app, it complexes more than I think. So I used the simplest pattern, I used scope model.
If must start one new flutter app more, I will use scope model again 👍

@ShashankSirmour
Copy link

Definitely a fan of the type safe approach. Is it possible to have a class that accepts an infinite number of types in Dart? I was unsure about it. The other piece that I was unsure of is how those items would be destructured in the builder.

After digging around Dart's documentation, I did find a pattern using mixins that seems equally robust w/o changing the api:

class CounterModel extends Model {
  var _counter = 0;

  int get counter => _counter;
}

abstract class IncrementModel extends CounterModel {
  void increment() {
    // First, increment the counter
    _counter++;

    // Then notify all the listeners.
    notifyListeners();
  }
}

abstract class DecrementModel extends CounterModel {
  void decrement() {
    // First, decrement the counter
    _counter--;

    // Then notify all the listeners.
    notifyListeners();
  }
}

abstract class ResetModel extends CounterModel {
  void reset() {
    // First, reset the counter
    _counter = 0;

    // Then notify all the listeners.
    notifyListeners();
  }
}

class MainModel extends Model
    with CounterModel, IncrementModel, DecrementModel, ResetModel {}

This is a contrived example but it simulates how you can compose an infinite number of models together (similar to combineReducers in redux).

why it's giving error now and giving an error like "The class 'ConnectedModel' can't be used as a mixin because it extends a class other than Object"

@linrz
Copy link

linrz commented Jun 2, 2019

I think this can help you. @ShashankSirmour

[Flutter/Dart] The class can’t be used as a mixin because it extends a class other than Object. Error [mixin_inherits_from_not_object]

@mbartn
Copy link

mbartn commented Apr 25, 2020

love scope_model approach, and yes , multiple model work perfect but with many models the init code it's a bit confused...

I was trying to solve multiple initialization problem and I've come up with this code

class MultipleScopedModel extends StatelessWidget {
  final List<Model> models;
  final Widget child;

  MultipleScopedModel({Key key, @required this.models, @required this.child})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    Widget mergedModels = child;
    for (var model in models) {
      mergedModels = ScopedModel(model: model, child: mergedModels);
    }
    return mergedModels;
  }
}

but for this initialization

class Model1 extends Model {}

class Model2 extends Model {}

void main() {
  runApp(MaterialApp(
    home: MultipleScopedModel(models: [Model1(), Model2()], child: Home()),
  ));
}

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Text('hello'),
      ),
    );
  }
}

the tree looks like this:
image

the problem is that these initialized models are seen by dart as of generic type Model, not my custom classes e.g. Model1 or Model2. It is not possible to get it by ScopedModel because it can't find type Model1 or Model2 in the tree.

The best solution would be something like

final List<dynamic extends Model> models;

but it is not allowed in Dart.

Do you guys have any idea how to solve this issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests