IIn keeping with the “KISS Principle”, this attempts to offer the MVC design pattern in an intuitive fashion incorporating much of the Flutter framework itself.
All in one lone Flutter Package:
Some Github examples right off the hop!
Right-click and save these examples as bookmarks. You may want to try out these examples later. They use this MVC implementation.
Counter app example — the counter app first created with a new Flutter project. This one, however, will use the MVC package.
Forget Everything I’ve Told You!
By the way, if you’ve been following me on Medium.com, forget the other two articles I wrote on the subject! Don’t read them! (Well, maybe the first bit of An MVC approach to Flutter.) I’ve moved on since then having learned a great deal and experimented on how to marry MVC with Flutter. I’ve made a Flutter Framework that I now use with all my apps! I’m satisfied for now. So much so, I’ve made it into this package and use it as the basis for all my apps.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots over gists to show concepts rather than just show code in my articles. I find them easier to work with. However, you can click/tap on them to get at the code in a gist or in Github if you must. Ironically, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program on our computers; not on our phones. For now.
No Moving Pictures No Social Media
Note, there will be gif files in this article demonstrating aspects of the topic at hand. However, it’s said viewing such gif files is not possible when reading this article on platforms like Instagram, Facebook, etc. Please, be aware of this and maybe read this article on medium.com
Like many design patterns, MVC aims to separate ‘the areas of work.’ It’s meant to separate ‘the areas of responsibility’ when developing software. With MVC, this means separating ‘the interface’ from ‘the functionality’ from ‘the data’ that makes up the application. To decouple these three major areas in the software allows for efficient modular coding, code reuse, and parallel development:
- Model — the source for data. In some implementations, it contains the business logic.
- View — concerned with how data (not necessarily that in the Model) is displayed in the user interface.
- Controller — controls what data is displayed, answers to both system or user events, and, in some implementations, contains the business logic.
“The model is. The view shows (what the model is). The controller changes (what the model is or what the view shows).” — Jon Purdy Jun 8 ’11 at 1:51
The version of MVC conveyed in this implementation is actually more of a PAC (Presentation-abstraction-control) pattern instead of the classic MVC. In the PAC pattern, the Controller contains the business layer while the Model is not explicitly defined but an abstraction that ‘stores data.’ The code in the ‘View’ does not directly affect or interfere with the code involved in the ‘Model’ and vice versa. While the ‘Controller’ controls the ‘state’ of the application during its life cycle. The View can ‘talk to’ the Controller; The Controller can ‘talk to’ the Model, but again, the View is unaware of the Model and vice versa.
However, this particular MVC implementation allows one to also implement the more classical pattern. One that allows the View and the Controller to ‘talk to’ each other, as well as ‘talk to’ (by calling public functions and public properties) the Model.
By Design, You Chose the Design
It’s to the developer to pick the appropriate approach depending on the circumstances presented to them during development. That’s what design patterns, frameworks, architectures, or whatever you want to call them are for. They are to allow you to organize the code, to impose your chosen structure, to allow for some conformity and some consistency.
The idea is that doing so will then allow a developer new to the development team, who is familiar with this package or at least familiar with the MVC design pattern, to walk in and already know where all the code is. The developer, with task in hand by their manager, can ‘hit the ground running’ and go straight into the application knowing how the code is arranged and how the boilerplate works — what little boilerplate there is in this case.
Note, there’s no Model
Note, I’ve purposely left any implementation of a Model out of this implementation of the MVC. Again, more in keeping with the PAC design pattern. You won’t find the concept of a Model in this package. There’s a ‘View Class’, and there’s a ‘Controller Class’, but there’s no ‘Model Class.’ If and how a Model is implemented, I leave to the developer.
When it comes to a Model, you may even choose to implement a hybrid of sorts (and all that entails). For example, one Dart file could contain what would be defined as both the Controller and the Model.
Maybe your app has no data! Maybe your app has lots of data! (i.e. any number of Models). Who knows! This package will allow for that — by not caring either way frankly.
The last graphic demonstrates another possible purpose for a Model. There may be times your app works with another app’s data source. Your Model serves to ‘convert’ the other app’s data to a format suitable to your app.
This is actually demonstrated by this library package contribution to the Flutter Architecture Samples Project. You’ll find the model in the MVC Example merely ‘relays’ the data requests to models used by yet another architecture design pattern. In this case, the Scoped Model.
In Flutter, There Are Many Views
From the beginning, the intent for this package was not only to allow the developer the freedom to write their code in the best way to meet their needs but also in a way that best utilizes the underlying Flutter framework.
In Flutter, a ‘Navigator’ is assigned its routes and is used to go ‘up the tree’ of Widgets to view one screen after another. Using this framework, a Controller can ‘follow along’ the list of possible Widgets (Views). Note, however, it can only access one View at any one time. In other words, a Controller can be assigned to any number of Views in the course of its own lifecycle, each one representing a Widget with its build() function, but can only directly access the ‘last’ View assigned. Understand? Don’t worry, this will be demonstrated in subsequent articles.
Note also, as any good MVC design should, you are allowed as many Controllers as you like to have access to a particular View. Each ready to react to any ‘events’ that originate from that View. Oh yes, there are many events fired in Widgets!
And so, if there is a number of Controllers assigned to a View, and when ‘an event’ occurs in that View, the Controllers will fire, in turn, in the order they were assigned.
Again, there are events fired in the course of a Flutter app’s lifecycle. See Listeners below.
How Does It Work?
So, how do you use this package? Well, let’s fall back on the ol’ ‘Counter app’ first introduced to you every time you create a new Flutter project. To utilize the package in the Counter app, I changed three things. I extended the Class, _MyHomePageState, with the Class StateMVC, I introduced a ‘Controller’ Class that extends ControllerMVC, and I then introduced a static instance of that Controller Class to the build() function. Done!
Now, of course, there’s a lot of things going on here, but you don’t see that. As any good library package should, a lot is kept in the background. At a glance, however, you can see there is now a separation of the ‘Interface’ and the ‘data.’ The build() function (the View) is concerned solely with the ‘look and feel’ of the app’s interface — how things are displayed. In this case, it is the Controller that’s concerned with what is displayed.
What data does the View display? It doesn’t know nor does it care. It ‘talks to’ the Controller instead. Again, it is the Controller that determines ‘what’ data the View displays. In this case, it’s the app’s title and a counter. Even when a button is pressed, the View turns to the Controller to address the event by calling one of the Controller’s public functions, incrementCounter().
How Do Things Relate?
Notice that in this arrangement, the Controller is ‘talking back’ to the View by calling the View’s function, setState(), to tell it to ‘rebuild.’
Maybe you don’t want that. Maybe you want the View to be solely concern with the interface, and it alone determines when to rebuild or not. It’s a simple change.
You see the function, setState(), is now called in ‘the View’ (in the build function) and not in the Controller. Doing so does separate the ‘roles of responsibility’ a little more, doesn’t it? After all, it is the View that’s concerned with the interface. It may know best when to rebuild, no? Regardless, with this package, such things are left to the developer. Also, notice what I did with the app’s title? I created a static String field in the MyApp class called, title. It is the app after all. It should know its own title.
How about Model?
Currently, in this example, it’s the Controller that’s containing all the ‘business logic’ for the application. However, in some MVC implementations, it’s the Model that contains the business rules for the application. So what would that look like? Well, maybe it could look like this:
I decided to make the Model’s API a little cleaner with the use of static members. As you can see, the changes were just made in the Controller. The View doesn’t even know the Model exists. It doesn’t need to. It still ‘talks to’ the Controller, but it is now the Model that has all the ‘brains.’
However, what if you want the View to talk to the Model? Maybe because the Model has zillions of functions, and you don’t want to write the very same functions in the Controller only ‘to relay’ the Model’s functions and properties over to the View. You could simply provide the Model directly to the View. The View could then calls the Model’s properties and functions.
Not particularly pretty. I would have just kept the Static members in the Model, and have the View call them instead (or not do this at all frankly), but I’m merely demonstrating the possibilities. With this MVC implementation, you have options, and developers love options.
How Does It Works?
When working with is MVC implementation, you generally override two classes: The Controller (Class ControllerMVC) and the StateView (Class StateMVC). Below is a typical approach to overriding the Controller.
The Controller has ‘direct access’ to the View (aka. the Class StateMVC). This is represented by the property, stateMVC, in the Class, ControllerMVC. In this example, a ‘static’ reference to the View. A ‘static’ reference to the Controller itself is made as well, in the Constructor. This example conveys a generally good approach when dealing with the application’s Controller because you then have easy access to your Controller throughout the app. It’s now in a static field called con. See how it’s easily accessed now in another Dart file below:
Now, in any View or any Widget for that matter, you can access the app’s data or business logic easily and cleanly through its Controller. For example, you can then reference the Controller in a build() function using the field, con.
Note, the ‘StateView’, like the State Class is abstract and must be extended to implement its build() function. So, in fact, ‘the View’, in this MVC implementation, is simply the State’s object’s build() function! Surprise!
Note, the Controller class (ControllerMVC) looks pretty thin at first glance. I mean the code you see there is only if a ‘StateMVC’ object is passed to the constructor. The Controller is then ‘assigned’ that State object. In most projects, the Controller will serve as your ‘workhorse’ containing most of the ‘logic’ for the app.
Set the State
The StateMVC class, of course, has the setState() function. It’s a State object after all. However, with this MVC implementation, you also have the setState() function in your Controller! That will prove to be very useful in development.
Both Classes, also have a refresh() function. To ‘refresh’ or to ‘rebuild’ the User Interface (The View). It’s a little trick. It’s just the function, setState() called with an empty closure. Cute.
Listen! There’re listeners!
Note, the ‘Controller’ class has a parent Class called _StateObserver. In turn, that class uses the Mixin, StateListener. It is this Mixin that allows the Controller to respond to events generated by the phone or by the user. As of this release, there are some twelve separate events available. Note, this Mixin is also available to you! Apply it, and you too will have a ‘State listener’ to use in your app. Interesting, no?
Mixin a Listener
Note, since version 1.0, the StateListener itself has changed from a class to a mixin. Why you ask? So you then have more options. We developers love options! You can then attach this Listener using the ‘with’ keyword to any class and use its code. It’s not a class, and so you don’t have to deal with the ‘one superclass per class’ limitation. Your class is free to extend another class, ‘implement’ any number of classes, and apply any number of mixins as well as become a ‘State Listener.’ Nice. And so the keyword, class, is now mixin.
Get Set To Listen
Below are the three functions used by the StateMVC class to register such Listener objects.
Of course, you have the means to remove a listener if and when it’s necessary.
Before and After
Note, the function, addListener(), does the same thing as the function, addAfterListener(). The ‘before’ and ‘after’ reference means you can assign listeners that will fire ‘before’ or ‘after’ the Controllers fire. Why? I don’t know! It’s your app! ;)
A Function for every Event
Below, you’ll see the twelve events available to you. Turn to the WidgetsBindingObserver Class for details. Frankly, you may never ever use all these events. A few you will use every time, however, most you won’t ever need to use. Regardless, this framework allows easy implementation if and when you do.
There’s functions Flutter apps will always trigger. Those include, initState(), didChangeDependencies() and dispose(). Still others will reflect different stages of the app’s lifecycle. These would include, didChangeAppLifecycleState(), deactivate() and didUpdateWidget(). They’re all listed below and more.
That Rule Again!
The same rule that applied to View’s and its Controller applies to its Listeners: A Listener can only access one View at a time. Again, a Flutter app, in the course of its lifecycle, with the use of its Navigator and its routes, will go up through a number of Widgets and back again. In other words, as routes are followed through the Navigator, the app will go up through a number of build() functions and back down again while it runs. The Listener will ‘record’ this progress, but will only access one View (one build() function) at a time.
Note, you use a Listener (i.e. A class that extends StateListener) only when you want some particular code to fire for some particular event in some particular View. And like Controllers, you can have any number of Listeners assigned to a particular View.
You’ve Got Class!
And so, let’s now list out all the public classes that are available to you when utilizing this MVC library package:
ControllerMVC — Your ‘working’ class concerned with your app’s functionality.
StateListener — The event handler in all the Controllers and Listeners.
StateMVC<T extends StatefulWidget> — The MVC State Object.
ViewMVC — An explicit View Class. Extend and implement its build() function.
StateViewMVC<T extends StatefulWidget> — Error Handler in its build()function.
AppMVC — The ‘View’ of the App passed to the runApp() function.
AppConMVC — The ‘Controller’ of the App passed to the AppMVC.
Uuid — For generating unique IDs.
The StateMVC Class
You’ll find the ‘MVC’ State object has all the attributes of an ordinary State Object and more.
context — The BuildContext passed in the build() function.
mounted — A boolean value indicating if the State object is terminating or not.
widget — The associated ‘StatefulWidget’ object.
build(BuildContext context) — An abstract method to ‘build the View.’
initState() — Called exactly once when the State object is first created.
dispose() — When the State object will never build again. Its terminated.
add(ControllerMVC c) — Add a specific Controller to this View.
addList(List<ControllerMVC> list) — Add a list of Controllers to this View.
deactivate() — When the State object is removed from the Widget tree.
didChangeAccessibilityFeatures() — When the system changes the set of active accessibility features.
didChangeAppLifecycleState(AppLifecycleState state) — Called when the system puts the app in the background or returns the app to the foreground.
didChangeLocale(Locale locale) — When the system tells the app that the user’s locale has changed.
didChangeMetrics() — When the application’s dimensions change. (i.e. when a phone is rotated.)
didChangeTextScaleFactor() — When the platform’s text scale factor changes.
didHaveMemoryPressure() — When the system is running low on memory.
didUpdateWidget(StatefulWidget oldWidget) — Override this method to respond when the Widget changes (e.g., to start implicit animations).
onError(FlutterErrorDetails details) — Allows one to define their own Error Handler. The default routine is to dump the error to the console.
reassemble() — Whenever the application is reassembled during debugging, for example during hot reload.
keyId — Returns a Sting object uniquely identifying this Controller.
addAfterListener — Adds an event listener that runs after the Controller(s).
addBeforeListener — Adds a listener that runs before the Controller(s).
addListener — Adds an event listener that runs after the Controller(s).
afterContains — Determines if a listener is in the ‘after’ listing.
afterList — Get a List of Listeners by a list of unique String id’s.
afterListener — Get an ‘after’ Listener by its unique String id.
beforeContains — Determines if a listener is in the ‘before’ listing.
beforeList — Get a List of Listeners by a list of unique String id’s.
beforeListener — Get an ‘before’ Listener by its unique String id.
removeListener — Remove a specific event listener from the ‘StateView.’
addList — Adds a List of Controllers to this ‘StateView.’
contains — Determines if this ‘StateView’ contains a particular Controller.
controllers — Returns a Map of Controllers by their unique String identifier.
remove — Removes a Controller from this ‘StateView’ by their unique id.
The ControllerMVC Class
A Controller has those ‘event’ functions listed above and more. Below are some additional properties and functions specific to a Controller:
addState(StateMVC state) — Add the Controller to the specified StateMVC.
setState() — Notify the framework the Widget Tree is to be rebuilt.
stateView — Returns the StateMVC object associated with it. Deprecated.
stateMVC — Returns the StateMVC object associated with it.
refresh() — Allows you to call setState() to rebuild the Widget tree.
afterListener (String key) — Retrieve Listener by unique id.
beforeListener(String key) — Retrieve Listener by unique id.
dispose() — Called when its ‘current’ State object is terminating.
How to Start Your App
Below would be a typical approach to start up an app using this package. This is done by using yet another Class in the MVC Pattern package. It’s called AppMVC.
Here’s a ‘Widget’ that you may want to supply to Flutter’s runApp() function at the start of the application.
Two ‘new’ functions exist solely for the ‘App Class’: initApp() and init(). A lot of things have to ‘start-up’ when starting up an App, and these two functions provide a place for them. The function, initApp(), is for ‘quick’ synchronous processes while the init() function returns a Future and so is for asynchronous operations. In such cases, usually a FutureBuilder() function is called upon. You see, one has to wait at the start of the app for such ‘asynchronous stuff’ to complete, and so a ‘Loading Screen’ is displayed while we wait.
The App’s State
Below is a snapshot of the last line of code in the AppMVC class. Since it extends the StatefulWiget class, the function, createState(), has to be implemented. Look at the State object that’s created.
A Stateful Build
Note, the little trick where the build() function found in the ‘StatefulWidget’, AppMVC, is called in the State object, _AppState. Further, notice a Controller could be passed to the ‘App’ Widget. Doing so, of course, allow one to add code to the ‘State listener’ portion of the Controller (StateListener).
There’s an Error Handler
Error handling is important. But like writing documentation, we programmers tend to neglect it. ( That reminds me, I’ve got to work on the package’s online documentation. It’s horrific! lol)
In any case, every developer is, of course, encouraged to not only use their try…catch statements judiciously in their code. But face it, your application will fail at one time or another. However, this framework does allow you to address error handling, and, for example, have your app fail in a more graceful fashion — maybe even email an error report somewhere.
The State of Error Handling.
In the framework’s ‘StateView’ (Class StateMVC) there’s an error handler. There can be a lot of Controllers assigned to it and anyone of them could cause an error. Therefore, there is an error handler at ‘State Level’ that a developer is invited to use to least try to handle things when something goes wrong. It’s the last function in the class, and you’re encouraged to override the onError() function to ‘catch’ any unexpected errors.
In an organization, maybe you’ve been assigned the ‘State Level’ stuff and you’re working in the class that extends the class, StateMVC. You’ll have other developers add in their Controllers to your State object to perform their independent operations. You have the means to ensure your State object will stay up if one of those Controllers causes an error. Note, this demonstrates how ‘modular’ programming is one of the benefits of using design patterns.
It’s Your Responsibility. Handle it!
Left on its own, if an error does occur, this framework’s default response is the usual ‘red screen of death.’ I went back and forth on the idea of ‘imposing’ my own error handler in this framework, but, in the end, I decided against it. It would go against the idea behind this framework: The developer has options. Besides, it’s the developer who would have a better idea of how to handle unexpected errors in their particular app! So again, the default response is preserved. It’s the developer’s responsibility to implement error handling.
Error Handling has been deemed so important, there’s yet an additional State Object Class called StateViewMVC. It extends StateMVC, but has it’s own error handling — right inside its build() function.
It does mean ‘breaking out’ a little bit since you’ve got to now extend three Classes instead of two. Remember, I said you generally override two classes: The ‘Controller’ (Class ControllerMVC) and the ‘StateView’ (Class StateMVC)? Well, there is…wait for it…another option. Love options!
There’s ‘another view’ with a name more suitable to this whole venture called, StateViewMVC. This ‘View’ is not a State object, but you still extend it to implement its build() function for your interface! Note, this ‘View’ extends the same class, _StateObserver, as the Controller!? Now, what does that mean?!
This StateView Handles Errors
Unlike its parent class, StateMVC, this class has an error handler right inside its build() function! This means, with all those Controllers and Listeners running in turn when ‘rebuilding your Widget tree’, you can be assured, if one of them misbehaves and an error occurs, the error handler you implemented will make every effort to fail gracefully or maybe, if possible, recover and continue! One day, I’ll be writing a separate article dedicated to this approach.
What’s This All Good For?
I mean, the general idea after all, when implementing MVC, is to adhere to the simple practice of only supplying ‘the data’ to the View through the Controller. In other words, in this implementation for example, when you glance into any build() function, any ‘data’ referenced in there should only be coming from an instance of the Controller(s). It should be coming from either a Controller’s static or instance members in the form of its fields, its functions and or its getters. Simple as that.
The Scoped Model applies the same practice. In both screenshots above, an import statement is used to defined the data type that will represent ‘the data to be viewed’ in their respective build() functions. You then retrieve that ‘data reference’ by their respective means and supply it to that Widget’s build()function. Simple as that.
Frankly, in the case of the MVC example, there’s no real ‘boilerplate’ to speak of. It’s apparent that ‘Dependency Injection’ is being applied in the form of a standard import statement. Frankly, you could get away with such an approach using a ‘vanilla version’ of Flutter.
So why would you use this library package? To access the app’s Lifecycle. That’s why. If anything, I’d suggest you use this framework so you then have access to the Lifecycle of your app…and then any associated Controllers has access to the app’s Lifecycle as well.
Remember the Controller class first listed in the article? I’ve listed it again below. It extends a class called, _StateObserver. Now what makes up that class, I wonder.
Ok, I Lied. There is More To It
Ok, it’s not just to access the app’s lifecycle. We can’t forget the bigger picture here. Like any good framework, this MVC library package does impose some consistency; some guidelines. It separates the code and tells you ‘where it lives.’ It allows developers to come up to the code, know what is where, and ‘hit the ground running.’
Finally and Freely
I give this package freely to our fledgling Flutter community. I saw a need. I feel it‘ll be a good option when starting your next mobile app. Always good to have options, right?
MVC By Example
Here are three example apps developed using MVC for your reference:
Further Readings on MVC includes a three-part series: