Flutter: State Management using an MVC+S Architecture

There are many ways to architect an app in Flutter, and just about as many state management frameworks out there to do it for you! With this in mind, we thought it might be nice to talk about how we build scale-able apps without a framework, using only the Provider package, and some simple application tiers.

NOTE: The first portion of this post is a pre-amble about MVC in general, the architecture we are using, and some rationale behind why we think it’s effective. You can skip this if you want and just get to the code, or follow along with the example over on github: https://github.com/gskinnerTeam/flutter-mvcs-hello-world

MVC+S Architecture

In classic MVC, by definition, the Model: “directly manages the data, logic, and rules of the application“, and it is common in MVC discussions to read statements like “business logic goes in the model“. In practice, this works for small components and views, but we find it breaks down quickly when applied to a full application.

This is because it overloads the models with too much responsibility. Unchecked, this can push you to scatter application-level logic across various views in your application, create confusion about where the code should go for any given behavior and lead to large Models that are hard to work with.

The main issue with the classic definitions of MVC, in an application context, is that the terms “business logic “or “rules of the application” are too ambiguous to be useful. Instead, applications tend to break down into Domain Logic (rules around data storage, manipulation, and validation) and Application Logic (what your application actually does. How it behaves.) You also find that:

  • Domain Logic naturally belongs in the Model tier.
  • Application Logic naturally belongs in the Controller tier.

This requires a robust “Controller” layer in your architecture that is separate from both the View and the Model. Something beyond basic view controllers, that is more “god-like”. A tier that can encapsulate any re-usable behavior or logical sequence of events in your app and be easily re-used across multiple Views.

To enable this tier we like to use things we call “Commands”:

  • Commands are application-level tasks (Bootstrap, RefreshData, Logout) that are encapsulated in an object instance and started with a run() call: await LogoutCommand().run();
  • Commands can be initiated from the View layer, based on UI events, or via other Commands in a chained fashion
  • Each Command encapsulates its own state and can be asynchronous. Perfect for doing complex tasks, or choreographing multiple Service calls.

Here’s a prototypical example of a Command being initiated by the View layer:

// Usage: View initiates command
onPressed: ()=> LoginCommand().run("user", "pass");

// Command does something... 
class LoginCommand(){
  Future<bool> run(String user, String pass) async {
    // Do everything in the app required to login
    // return results to the caller when we're done
    // Now many views can easily trigger/share the same complex login logic
  }
}

We’ll talk more on Commands in a bit, but first, we need to touch on Services. This is another thing not classically handled by MVC, which is fetching data from, or interacting with, the outside world. The “outside world” being defined as anything outside your application sandbox, be it internet services, or operating system calls. To serve this, we use a fourth conceptual tier which is the Service Layer.

  • Services are concerned only with making external API calls, and parsing/returning any data they receive back. These are usually internet-based calls but can also include native OS-level interactions via native extensions.
  • Services never touch the Model directly. Instead, Commands will make Service calls, and update the Model with the results, if they choose.

This is sometimes referred to as the Model, View, Controller + Services, or MVCS architecture. Typically these architectures would have a ViewMediator layer which proxies inputs and data back and forth between Model and View, however due to Flutter’s code-based layout tree, and easy data-binding with Provider, we decided to remove this layer completely and simplify the architecture further.

Here is a visual representation of the entire MVCS flow as we use it:

Notice that our View layer maintains a conceptual “weak link” to both the Services and the Models from the View. We don’t prevent these tiers from being accessed from the View tier, but generally it’s recommended to use a Command to interact with Services and change Models, as opposed to having a very smart View Controller.

We’ve found this approach quite effective at building large apps on other platforms, and were anxious to try it out in Flutter!

First, let’s be clear…

Model and Controller may be the two most overloaded terms in programming (sorry Component!), so let’s start with some definitions:

  • MODEL – Holds the state of the application and provides an API to access/filter/manipulate that data. Its concern is data encapsulation and management. It contains logic to structure, validate or compare different pieces of data that we call Domain Logic.
  • VIEW – Views are all the Widgets and Pages within the Flutter Application. These views may contain a “view controller” themselves, but that is still considered part of the view application tier.
  • CONTROLLER – The controller layer is represented by various Commands which contain the Application Logic of the app. Commands are used to complete any significant action within your app.
  • SERVICES – Services fetch data from the outside world, and return it to the app. Commands call on services and inject the results into the model. Services do not touch the model directly.

With Services in mind, here is a slightly more concrete example tying it together. A Command is triggered from a View somewhere, the Command makes a service call, and updates the Model. The View will get rebuilt automatically with Provider (more on this later).

//View controller, triggers command...
void _handleRefreshTapped() async {
  List<Posts> posts = await RefreshFeedCommand().run();
}


// Command runs...calls service...updates model
class RefreshFeedCommand(){
  Future<List<String>> run() async {
    var list = await someService.getPosts();
    someModel.posts = list;
    return list;
  }
}

// Any views bound to the model will be automatically updated

This is an very simple example, but this code is already highly portable within your app, easy to maintain / debug, and in a good position to grow as the app becomes more complex.

For more complex examples check out the some of the production commands from our open-source Desktop App “Flokk”. In these, you can see precisely the type of high-level logic that can accumulate during a project. You can also see some pretty intense chaining in action!

Why use Commands?

Before going further, we should talk about why Commands work so well in this context:

  • Commands remove the business logic from the Model. Models can get huge when just managing data (+ an API to work on that data). From our experience, it never scales well trying to have application logic and state combined in one class, you always need to split these up one way or another as things scale.
  • Commands are totally encapsulated. Because each command is its own little object, they completely protect their own state and never collide with other Commands. They can be long-lived, or short. They can store internal state easily allowing things like cancel() or undo(). As opposed to a static function, or a Model method, you can run any number of concurrent Commands, and they will never collide.
  • Commands provide a clean API for Application Logic. Because Command.run() is just a function, you can declare directly the options or dependencies you need to complete the action. This makes it very clear to the caller what is required and no need for Data Transfer Objects or un-typed payloads to move parameters around.
  • Commands are easily chain-able. It’s easy to combine multiple commands into a larger one, allowing for a lot of flexibility when composing your business logic. For example, you could have 3 different RefreshXCommand, and then compose those into a single RefreshAllCommand very easily.
  • They scale well. Because each command is isolated in its own file, they can grow quite complex and still be easy to work on and debug.
  • Working with multiple data sets is easy. Commands can access any combination of models and services to do what they need to do. This removes most dependencies between models, and creates a clear domain layer where these types of high level actions should exist.
  • The code is easy to maintain. This is probably the biggest win for Commands. When returning to a project weeks or months later, it’s extremely easy to jump in and find what you need to work on, as all the high-level business logic is wrapped in these dedicated class files with very clear names. The value of this can not be overstated.

Code Time

Ok, enough talk, let’s see some code!

In this example, we’ll create a 2-page app. Although simple, this should allow us to demonstrate all 4 tiers of the application.

The app will consist of:

  1. A LoginPage with a button which triggers a LoginCommand
  2. LoginCommand will make UserService.login call
  3. If UserService.login is successful, Command will set AppModel.currentUser and initiate the RefreshUserPostsCommand
  4. RefreshUserPostsCommand will run, and update UserModel.userPosts
  5. Our AppScaffold is bound to currentUser and will show HomePage whenever currentUser != null,
  6. HomePage is bound to userPosts and will display a list of posts if there are any.
  7. HomePage will also have a “refresh” button that triggers the RefreshUserPostsCommand each time it is pressed

This demonstrates:

  • Initiating a Command directly from a View
  • Using a Service inside a Command and then updating the Model
  • Spawning a Command within another (chaining)
  • Using a command in multiple places in the app
  • View:Model binding

As we step through the different application tiers, you can follow along with the complete project on github if you like.

Model Tier

First, let’s take a look at the Model tier of our application. We’ll define two models, AppModel which stores the currentUser, and UserModel, which stores the user’s current posts.

class AppModel extends ChangeNotifier {
  String _currentUser;
  String get currentUser => _currentUser;
  set currentUser(String currentUser) {
    _currentUser = currentUser;
    notifyListeners();
  }
  // Eventually other stuff would go here, appSettings, premiumUser flags, currentTheme, etc...
}

class UserModel extends ChangeNotifier {
  List<String> _userPosts;
  List<String> get userPosts => _userPosts;
  set userPosts(List<String> userPosts) {
    _userPosts = userPosts;
    notifyListeners();
  } 
  // In the future, this would contain other data about Users, maybe active friend lists, or notifications, etc
}

Nothing special here, just a couple of ChangeNotifiers with encapsulated fields, that call notifyListeners when they are changed.

Service Tier

We can look at the Service layer next, which is very simple as it’s just mocking data for now. We’ll make a fake login() call, and a fake getPosts():

class UserService {
  Future<bool> login(String user, String pass) async {
    // Fake a network service call, and return true
    await Future.delayed(Duration(seconds: 1));
    return true;
  }

  Future<List<String>> getPosts(String user) async {
    // Fake a service call, and return some posts
    await Future.delayed(Duration(seconds: 1));
    return List.generate(50, (index) => "Item ${Random().nextInt(999)}}");
  }
}

Command (Controller) Tier

The glue between the Model and the Service is the Command layer. Before writing our first command, we usually create a Base class (or Mixin), as syntactic sugar, to provide our Commands the dependencies they need:

BuildContext _mainContext;
void init(BuildContext c) => _mainContext = c;

// Provide quick lookup methods for all the top-level models and services. 
class BaseCommand {
  // Models
  UserModel userModel = _mainContext.read();
  AppModel appModel = _mainContext.read();
  // Services
  UserService userService = _mainContext.read();
}

The first thing you might notice, is init function at the top. To keep things testable using Provider, the Command tier relies on someone to pass it a BuildContext it can use to fetch dependencies. This will be done in main.dart, as we’ll see in a bit.

Another thing to note, is that we don’t declare any actual interface for the Command function. All Commands are expected to implement a Future<T> run() async {} function, but we leave the specific function signature up to each concrete Command. This is done to keep things simple and maximally flexible.

Now that we have a base command which gives us a little syntactic sugar, we can create a command that logs us into the app. Extending the BaseComand, the LoginCommand might look like:

class LoginCommand extends BaseCommand {

  Future<bool> run(String user, String pass) async {
    // Await some service call
    bool loginSuccess = await userService.login(user, pass);

    // Run a 2nd command if service call was successful
    if (loginSuccess) {
      await RefreshPostsCommand().run(user);
    }
    // Update appModel with current user. Any views bound to this will rebuild
      appModel.currentUser = loginSuccess? user : null;

    // Return the result to whoever called us, in case they care
    return loginSuccess;
  }
}

If the Login was a success, it will kick off another Command. The RefreshPostsCommand would look something like:

class RefreshPostsCommand extends BaseCommand {

  Future<List<String>> run(String user) async {
    // Make service call and inject results into the model
    List<String> posts = await userService.getPosts(user);
    userModel.userPosts = posts;

    // Return our posts to the caller in case they care
    return posts;
  } 
}

View Tier

Ok… we’re almost done! All that’s left now is to create the view that will represent the Model and fire off new Commands.

First, declare main.dart, and “provide” the Models and Services you need:

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext _) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (c) => AppModel()),
        ChangeNotifierProvider(create: (c) => UserModel()),
        Provider(create: (c) => UserService()),
      ],
      child: Builder(builder: (context) {
        Commands.init(context);
        return MaterialApp(home: AppScaffold());
      }),
    );
  }
}

Hopefully, the above code is pretty easy to follow. We’re providing two Models and one Service to the tree below. We also use a Builder to get a context that we pass to Commands.init() which allows the Command layer to access any of the provided Models or Services.

Also defined in main.dart we have a simple AppScaffold:

class AppScaffold extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Bind to AppModel.currentUser
    String currentUser = context.select<AppModel, String>((value) => value.currentUser);
    
    // Return the current view, based on the currentUser value:
    return Scaffold(
      body: currentUser != null ? HomePage() : LoginPage(),
    );
  }
}

Note that we are binding directly to the AppModel.currentUser property here. This means this AppScaffold View will rebuild whenever appModel.currentUser changes, giving us a nice uni-directional data flow, all being handled by Providers context.select<T, T2> API!

Next up is the Login Page, which contains a single button, which triggers the LoginCommand, and it also has a loading spinner for when the LoginCommand is executing:

class _LoginPageState extends State<LoginPage> {
  bool _isLoading = false;

  void _handleLoginPressed() async {
    setState(() => _isLoading = true);
    bool success = await LoginCommand().run("someuser", "somepass");
    if (!success)  setState(() => _isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: _isLoading
            ? CircularProgressIndicator()
            : FlatButton(
                child: Text("Login"),
                onPressed: _handleLoginPressed,
   ),),);}}

The final view is the HomePage, which displays the results of the RefreshPostsCommand from the LoginCommand. It also contains a button of its own, which will trigger a new RefreshPostsCommand:

class _HomePageState extends State<HomePage> {
  bool _isLoading = false;

  void _handleRefreshPressed() async {
    // Disable the RefreshBtn while the Command is running
    setState(() => _isLoading = true);
    // Run command
    await RefreshPostsCommand().run(context.read<AppModel>().currentUser);
    // Re-enbable refresh btn when command is done
    setState(() => _isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    // Bind to UserModel.userPosts
    var users = context.select<UserModel, List<String>>((value) => value.userPosts);
    // Disable btn by removing listener when we're loading.
    VoidCallback btnHandler = _isLoading ? null : _handleRefreshPressed;
    // Render list of widgets
    var listWidgets = users.map((post) => Text(post)).toList();
    return Scaffold(
      body: Column(
        children: [
          Flexible(child: ListView(children: listWidgets)),
          FlatButton(child: Text("REFRESH"), onPressed: btnHandler),
        ],
),);}}

And with that, we get this absolutely spectacular MVC+S app, running in Flutter!

ARE YOU NOT IMPRESSED!?

So to recap, what we end up with here is:

  • A scale-able app architecture that can grow for a long time without feeling crowded or complicated
  • A codebase that is easy to maintain when returning in the future. Code is easy to find and application logic is hoisted fully outside the structure of the views in your app
  • Robust, uni-directional data flow with single-line data binding
  • Very little boilerplate, and easy to debug. The code is direct and to the point

Closing thoughts on Flutter/Dart

As we built this out, we were impressed with how, in many ways, Flutter is the perfect fit for this type of architecture:

  • Data Injection & View Binding: Provider is widely used, and gives an excellent method of dependency injection and view binding in one which are the backbones of any robust architecture.
  • Strong coupling between declarative and imperative view code: The mix of declarative and imperative code all in the same file, along with the easy view binding API, eliminates much of the need for ViewControllers or Mediators for your views.
  • Async support: Dart’s async language support is extremely robust. You don’t have to worry about anything getting GC’d before it’s done, and no messy event listeners or callbacks to deal with. Async Commands combined with Async Services are basically a dream to code.

What’s next?

We’ve thought about releasing a micro-framework, but really there’s nothing to release… The implementation is so simple, and Provider is really doing most of the work for us, we’re not even sure what a framework would look like. With that said, we are hoping to open-source a production-ready app-scaffold in the near future, which will be built off this style of architecture, but also include common things like styling support, app navigation, theming, and basic data serialization.

In the end, this is mainly meant to demystify state management a bit for you in general, and demonstrates the ease to which you can roll your own reactive architectures in Flutter without relying on some existing framework. Cheers!

shawn.blais

Shawn has worked as programmer and product designer for over 20 years, shipping several games on Steam and Playstation and dozens of apps on mobile. He has extensive programming experience with Dart, C#, ActionScript, SQL, PHP and Javascript and a deep proficiency with motion graphics and UX design. Shawn is currently the Technical Director for gskinner.

@tree_fortress

22 Comments

  1. Excellent write up, and a very good introduction to your MVCS architecture. With all of the good state management techniques out there, I am actually loathe to pick one. At least your architecture isolates the State (notifications and discoverability) to very specific sections; it will be easy to change from one to another. I am actually interested in using this approach using only Flutter’s InheritedWidget/ChangeNotifier constructs because I don’t feel comfortable picking a winner. In some respects, Provider is at the top, but now with RiverPod introduced, I suspect that will be the new direction for many.

  2. Ya, certainly would not be hard to build this using Inherited Widget, just a bit more boilerplate. At the end of the day though, it’s not like Provider someday stop working, it’s just a tool to bind `notifyListeners` > `setState`, and pass objects around the tree. I don’t see any reason why you’d really want to swap it out later for some other flavor of the same thing.

    I don’t personally expect riverpod to displace Provider, but we’ll see!

  3. I can very well agree with this solution. Really love the approach towards separation.
    Putting business logic in models never really scales well. We have been thinking of objects as packets that we transact in (pass to where it is needed).
    I also like how you have explicitly used Provider just for dependency injection / view binding.
    At Cookytech we have used streams to mediate between the controller and view layers. You still use commands(for us, simple functions) from within the BLoCs and use streams to notify the providers of state changes.
    This really leverages the declarativeness of the flutter framework and allows us to write mostly decision based synchronous code in the UI Layer.

    All in all there are some really great ideas here which we will definitely use to reinforce our architecture practices.

  4. Is there any disadvantage to adding Command parameters to the constructor rather than the run function. The benefit is that you could make AbstractCommand generic and force run to be implemented.

    Example:

    https://gist.github.com/chimon2000/697a41a30da74ecce1abf541dabc132c

  5. Fantastic article. Very much looking forward to the production-ready app-scaffold that includes styling support, app navigation, theming, etc.

  6. The reason behind my “other-than-provider” comment is, I would like to deliver some component suites for our company’s practice of federated development. The projects which consume these components may be using Provider, and some may be using (insert one of a hundred other SM* frameworks here).

    *not sadomasochism, but sometimes feels that way

  7. Awesome !
    Finally a solid architecture to serve as a basis for a Flutter application.
    Thank you so much Gskinner team.

  8. Can’t wait for the future! Would love to see your full featured demo app’s architecture.

  9. Matthew Aaron Taylor October 20, 2020 at 8:16pm

    it would be great to see some basic firebase examples. Database read/set/update, auth login, analytics, and crash reporting

  10. Thank you for sharing so much information with us, the flutter devs. I was looking on flokk code and what I saw is that or the models are available from the top of the application (from main.dart). But for some view section the provider/model should be created only when you navigate to them.
    Let’s say that contactsModel is used only on Contacs section of the app(no need in other sections, not from the “start” of the app).

    abstract class AbstractCommand {
    ….
    AbstractCommand(BuildContext c) {
    /// Get root context
    /// If we’re passed a context that is known to be root, skip the lookup, it will throw an error otherwise.
    context = (c == _lastKnownRoot) ? c : Provider.of(c, listen: false);
    _lastKnownRoot = context;
    }
    …..
    ContactsModel get contactsModel => getProvided(); //added from main.dart ChangeNotifierProvider.value(value: contactsModel),
    …..
    }

    Do you have any solution for this situation? No to have all models declared from the start of the app because this models are used only if the user lands on those section and safely recreated every time.

  11. Hey,

    greate pattern for flutter, feels good to work with! Thank you for your time and work!

    But I have a question in combination with clean code. A rule is to separate the ui and the business logic and preserve the testability.

    But I can’t use unit tests to test the single commands, beacause i need to setup the providers bec of the basecommand.. And the provider needs a Buildcontext. So regarding to this I need to use a widget test. So its kind of a coupling between the ui and the business logic right?
    Thank you for your help.

    Best regards,

    Robin

  12. Would I be mistaken thinking that there is a bit of RobotLegs in here?

  13. There is a “data” folder in flokk project. Any reason why is it not mentioned in the MVCS concept above?

  14. Master class on architecture in flutter, very clean and without dependencies on other packages, I am new to flutter and I have been reading about architecture of flutter MVVM, clean architecture, blocs,… etc for several days and by far from my humble opinion this is the best architecture to choose for a flutter project. I don’t have to search anymore, this is the one I’m going to choose. Excellent work, excellent article. Congrats!

  15. Mr. Anderson May 1, 2021 at 6:45am

    Fantastic article.

    Just that it looks like a link is not working properly (the complete project on github).

    Many thanks for sharing this.

  16. Yes it’s exactly true, it’s a FANTASTIC DESIGN PATTERN.
    Now we *need an article on medium.com* to zoom in on the implementation of this pattern.

  17. Mr. Anderson May 1, 2021 at 11:40pm

    Hello, I’m refactoring my app (I’m learning flutter) to implement this pattern (before I just had a view tier and a model/controller tier using Provider).

    My question is, how would you implement this pattern when you are calling the same command multiple times from the same widget as you don’t need to create a new instance of that command object every time? In my case, I’m trying to implement it in a music player app where I have a ListView widget with Tiles. When a Tile is tapped, I play the song, but if I tap another Tile another song start to play simultaneously because I’m creating a new instance of my command when tapping a Tile. I thought implementing singleton on that command, but I really don’t want to that as I think I should make a better use of Provider.
    Another work around I thought is just calling Provider.of() from my view which works, but would not follow this pattern.

    Many thanks for your help!!!

  18. Seems great, really but same question as Mr.anderson ::
    “how would you implement this pattern when you are calling the same command multiple times from the same widget as you don’t need to create a new instance of that command object every time?”
    if i well understand, the several command classes are replacing the “classic” controller one with it’s different functions also being of Future type…
    A controller is made a Singleton (.internal())
    So why don’t those command Classes aren’t Singleton themselves ?
    Am i getting something wrong ?

  19. First off, there’s nothing wrong with singletons when used judiciously, and if you feel like it best serves your use case, then you can go ahead and use one. If you’re looking for a way to injest testable singletons, check out the `GetIt` package which is an alternative to Provider, that does not depend on the widget tree. I’m not sure most Commands typically fit this pattern though.

    In short, the reason for not making them all singletons is flexibility and robustness. By using instances we can avoid any conflicts in state, we can have multiple versions of the same commands run, they each run independently, and we don’t have to worry about clashes or calling a command interrupting a previous command that is in-progress. It’s more flexible because we can have fully encapsulated commands if we want, or use static fields to have shared fields across commands.

    In the case of the music player, I would expect that any call to `StartSongCommand().execute()` would make a call to `songService.stop()` before starting the new song. It’s the music player service that is the singleton here, not the little commands that start or stop a given track.

    You can also have static fields, that all instances of a command share. For example, we use a `StartPoll` command, that has a static `timer` field, making sure that no matter how many times `StartPollCommand.execute` is called, it can only ever create a single timer.

    Finally, it’s worth noting that because these are simple objects, you can of course store them off in your view, and re-use them, if that makes sense for some reason.

    UploadVideoCommand command = UploadVideoCommand();
    ...
    void handleButtonPressed() => command.execute(someVideoPath);

    void cancelUpload() => command.cancel();

  20. Hi Shawn and thanks for your answers,
    I’m actually implementing your solution in my Fluttter project and have to admit i was quite surprised/confused about some points :
    1- when using Providers, do we have to code a third party ModelClass that extends “ChangeNotifier” to wrap our entities ?
    I thought that it would be our entities that would directly extend changeNotifier… but not so sure anymore after reading your code…
    i.e : if we have an “Person.dart” entity and a “ContactsPage” that manipulates a List then where would be the provider on (who extends changeNotifier?) ? And where would it be declared ?

    I’m a little bit confused because in your app you simulate the User Login thanks to an “app_model.dart” directly in “my_app.dart” And then you use an “user_model.dart” into homepage
    So how to decide what and where to “Provide” things ??

    2- You said : ” If you’re looking for a way to injest testable singletons, check out the `GetIt` package which is an alternative to Provider”
    It also surprised me to “provide” services, so it actually acts like GetIt would and can be used as a dependency injector like Dagger for Android ?
    Does that mean it’s absolutly sure that every provided services are only instanciated once ?

    PS :: I’m a 4 years XP french Android dev but only 3 weeks Flutter one and if some things are very similar, some others (like the state and its management) are very different to me :s

  21. OK, got the answers to my previous post on my own.
    But here is a last one (question) ::
    – You give a MultiProvider to give UserModel && AppModel and their changeNotifiers, ok.
    – Now if you add a “detailPage” and a “detailModel” representing the detail of the clicked post or whatever you want…
    And you use that code :
    >> “Navigator.pushNamed(context, DetailPage.routeName);”
    to push the new screen over the current, you’re now making a screen stack.

    What about the declaration of the new model and its changeNotifier ?
    Would you still provide them in the main.dart ???
    And if not, how would you do please ?

    Thanks in advance.

  22. PS :: ATM i’m ::
    > declaring the changeNotifier in main’s MultiProvider
    > in the build method of the DetailPage, i retrieve the model by ::
    final _detailModel = Provider.of(DetailModel)(context);

    After several tests, it works and only rebuild this very widget (DetailPage)

    But is this the right/better way to achieve this ?

Comments are closed.