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

7 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.

Leave a Reply

Your email address will not be published. Required fields are marked *