Flutter + MVC at Last!
The “kiss” of Flutter Frameworks.
In keeping with the “KISS Principle”, this is an attempt to offer the MVC design pattern to the Flutter community 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.
Startup_namer example — ‘Your First Flutter App’ but with this 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 as the basis for all my apps.
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. Ideally, 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’ (call public functions & 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 being that doing so will then allow a developer new to the development team, who is familiar with this package or at least 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 version of the MVC. 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 a 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 maybe another app’s data source. Your Model serves to then ‘convert’ the other app’s data to a format suitable to your app.
This is demonstrated with 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 actually used by another architecture design pattern. In this case, the Scoped Model.
In Flutter, There’s 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 latter’ 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’ one assigned. Understand? Don’t worry, this will be demonstrated in subsequent articles.
Note also, like any good MVC design should, you are allowed to as many Controllers as you like have access a particular View. Each ready to react to any ‘events’ that originates from that View. Oh yes, there’s 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.
There are defined events fired in the course of a Flutter app’s lifecycle. See Listeners below.
How’s 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. Like 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. 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 the Controller there merely ‘to relay’ the Model’s functions and properties over to the View. You could simply provide the Model to the View. The View then calls the Model’s properties and functions directly.
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’s 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 it’s 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 ‘work horse’ 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 response 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 an ‘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 a 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
We’re running in Flutter. Might as well use it. You’ll find the ‘MVC’ State object has all the attributes of a 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.
didChangeDependencies() — When a dependency of this State object changes.
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 therefore 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 any one of them can 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 a 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 a means to ensure your State object will stay up if one of those Controllers cause an error. Note, this demonstrates how ‘modular’ programming is one of the benefits to 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 on 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?!
That means they both can address events, and they both ‘rebuild’ the Widget tree. Nice.
The Other StateView Class
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 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.
Well, as you see below, the _StateObserver class is made up of our old friend, StateListener. One of two mixins that’s applied to ControllerMVC class. Now it’s all coming together. So you know that your Controller can apply code to every event in the app’s lifecycle as well. Neat.
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?