It’s Not Flutter
Keep It Simple, Keep It Flutter
I don’t use BLoC. I always felt using a Stream was a bit of overkill to call a setState() function whenever a change-in-state. Yes, it’s a means to do so independent of the Widget tree, but Streams were meant for streams of data not for event handling. However, my real issue is with the ‘Provider Model’ design pattern used by BLoC.
It’s not Flutter.
In other words, it doesn’t resemble the design pattern taken up by the Flutter framework itself. In truth, I‘ve no real issue with the Provider Model design pattern. It’s a proven approach conceived back in 2005 by Microsoft with the release of ASP.NET 2.0. It’s just not the design pattern found in Flutter.
Like any good framework, Flutter doesn’t impose its chosen pattern allowing developers to implement BLoC, RiverPod, etc. The design pattern they use is very familiar to developers. However, it’s my opinion using one that resembles the underlying framework will make developing and maintaining an app that much easier, that much more efficient and effective.
The design pattern taken up by the Flutter framework is like the MVC design pattern, but more like the PAC (Presentation-Abstraction-Control) design pattern. Flutter’s emphasis is more on the Interface (Presentation), a little on the logic and event handling (Controller), and less on data (Model). In Flutter, data is an Abstraction. It can be anything; It can be nothing. It’s implementation is left to the developer.
Further, Google engineers have only issued a Controller class to a select number of Widgets so far (see Google search below). With these Widgets, their build() functions will work the interface while their Controller class will work the widget’s data, logic and or event handling.
site:flutter.dev insubject:”controller”
In this article, a controller is supplied to the State class. That Controller will then have a reference to the State object allowing for a very powerful capability. A capability coveted by all those State Management solutions out there: Allowing for the change-in-state to be conveyed at any time and from anywhere. See below.
controller.state.setState((){});
The class, StateX, will be introduced in this article — it comes from the package, state_extended. Its constructor can take in a ‘State Object Controller’ and will then reference that Controller using the property, controller. See the screenshot below for an example.
This separate Controller class now has a reference to a State object and all that that entails. Make up your own State Object Controller, by extending the class, StateXController, and further receive a variety of properties and capabilities:
con.state; The current State object assigned to the Controller.
con.initAsync(); Run asynchronous operations before State build() runs.
con.rootState; The 'AppState' object. The app's first State object.
con.firstState; The first State object ever assigned to this Controller.
con.dependOnInheritedWidget(context); Register widget to InheritedWidget.
con.dataObject; Optional 'data object' passed down through the app.
con.inDebugMode; Boolean if running in Production or not.
con.lastContext; The lastest context object in the Widget tree.
con.lastState; The latest State object assigned to this Controller.
con.forEachState((state){}); Loop through its State objects.
con.notifyClients(); Call the built-in InheritedWidget.
con.ofState<_MainPageState>(); Returns specified State object.
con.setState((){}); Calls current State object's setState() function.
con.setBuilder(); setState() will rebuild the Widget from this builder.
con.stateOf(); Returns the StatefulWidget's State object.
Further note, it’s a Mixin that supplies much of this functionality to these Controllers (see below). If you don’t want to use the class, StateX, you can use the Mixin instead.
I‘ve migrated the ‘weather’ example app from BloC to this ‘MVC-more-like-PAC’ approach using the Fluttery Framework package. In this article, I’ll describe the differences between these two implementations.
Both GitHub repositories are available below:
GitHub - bloc/examples/flutter_weather
GitHub - Andrious/flutter_weather
Control Your Build
Below is a side-by-side comparison of the example app’s Settings page. The first screenshot below is the original BLoC version with its BlocBuilder() function while the second screenshot uses the StateXController class, and its setBuilder() function. Both are functions that rebuild with a setState() function call leaving the rest of the interface untouched.
Note how some of the logic (how values are conceived) is more readily visible in the BLoC version. For example, in the first screenshot, the property, WeatherState, is exposed in the interface while the Controller version in the second screenshot only displays designated properties and necessary functions from the Controller object, _con.
Supplying such a layer of abstraction encourages better modularity, better adaptation, better scalability, and generally better maintenance of the software. It provides the interface that's needed without revealing how it’s provided. That way, in the future, you can change how that’s done with ease without changing one bit of the interface.
Inherited Builds
In the BLoC version, the BlocProvider implements an InheritedWidget and makes the app’s remaining interface, WeatherAppView, a dependency of that InheritedWidget — a means to ‘rebuild’ that Widget again and again when necessary. See the first screenshot below.
In the second screenshot below, the StateX version utilizes an InheritedWidget as well. However, the widget, WeatherAppView, is replaced by the State object, _WeatherAppState, that defines the overall ‘look and behavior’ of the app.
This may be an unfair comparison at this point, as the Fluttery Framework is designed to produce ‘production-ready’ apps while the BLoC version is merely a very simple example app. Therefore, I’ll limit this article to comparing the ‘differences’ made to conceive the same interface and behavior. For more information on the Fluttery Framework, see the ‘Little More’ Series.
Control is Everything
Back to the BLoC version, as a dependency, the WeatherAppView widget will be rebuilt again if and when the value, toColor, has changed. You can see that highlighted in the first screenshot below. Again, the logic is exposed in the interface.
The next two screenshots below are from the Fluttery version of the app but are not the corresponding interface. Instead, it’s of the controller, WeatherController. That’s because that specific logic is found in the Controller and not in the interface. Again, it is the Controller that determines what widget is rebuilt and why.
In the BLoC version, the class, WeatherCubit, is somewhat equivalent to the class, WeatherController, in the Fluttery version. Both fetch new Weather information, and both deal directly with the data source involved.
A Single Simple Solution
Note, how the Controller uses a factory constructor to make only one instance of this class. This implements the Singleton design pattern and is, more often than not, very suitable for the role of a State Object Controller: It retains ‘a running account’ of the app’s state, its logic, and the current data involved. Further, it makes that one instance readily available throughout your app if you like using very little boilerplate code. Much of what makes up the Provider Model, is instead achieved by a factory constructor. Keep it simple. Keep it Flutter.
context.read<Weatheruit>.refreshWeather();
vs.
WeatherController().refreshWeather();
Going back to the example app comparisons. You can see in the first screenshot below, how the BLoC version relies on the Widget tree to retrieve the one instance of WeatherCubit. This is so as to call its functions, refreshWater() and fetchWeather(). The State object in the second screenshot below merely uses the local variable, con. It originally came about from the constructor call, WeatherController().
extension ReadContext on BuilldContext {
T read<T>() { return Provider.of<T>(this, listen: false); }
}
The same approach is found in the Settings page respectively.
In The Beginning
Note how the WeatherCubit class is defined nowhere near where it is first used. This practice is a throwback to when the Provider design pattern was first introduced with ASP.Net 2.0. Generally, everything was defined at the start of the app.
In the BLoC example app, the BlocProvider will only create an instance of the WeatherCubit class when it’s first required. When instantiated, it’s then stored in an internal variable, _value(second screenshot below). In the first screenshot below, the WeatherCubit class is defined at the start of the app. For this app, it is only stored in the variable, _value, (making for a pseudo-Singleton approach) when first required to determine the app’s background color (third screenshot below).
In the Controller version, a WeatherController instance is only created when it’s first required as well. With its factory constructor, it is that instance that is then used throughout the app. Done.
At the start of this example app, the *inTheme *clause is used to supply the color scheme — one of many options available to you when using Fluttery (see Little More Adaptive). In the second screenshot below, of course, it’s the same instance used to fetch the weather info.
The Hive Mind
By default, the BLoC version uses a popular lightweight NoSQL database called Hive. As such, working through the class, WeatherCubit, the Weather object’s data is saved to this database assumingly to demonstrate that capability. Thus, I noted when restarting the app, it wouldn’t fetch that city’s current weather info., but simply retrieve the past data stored in the Hive database. It may make for an out-of-date weather reading, but again, it demonstrated that capability. In the video below, the weather is only updated with another explicit fetch of the data.
Fluttey doesn’t offer a database. Like Flutter, that’s left to the developer. As for the example app, I chose only to save the city name using the device’s stored-preferences infrastructure. The next time the app started up, that city name would be looked up again with the usual API call.
In the first screenshot above, Felix chose to initialize the database at the beginning of the app. Again, a common practice in the past. The Fluttery version is more State-centric. The data source is only required in the State object, _WeatherPageState, where data is retrieved (first screenshot below). Of course, in the second screenshot below, it is its Controller that takes care of all this. Because the Controller was passed into that State object (see line below), its initAsync() function is called and performs the fetch. A spinner is displayed until that weather info is fetched from the Web service (see video below).
_WeatherPageState() : super(controller: WeatherController()) {
Again, I chose only to save the city name using the Prefs package. It works with the plugin, shared_preferences. which, depending on the platform, wraps the NSUserDefaults (in iOS) and the SharedPreferences (in Android), to provide persistent storage.
Let’s leave it there.
Note, if this app supplied real-time forecasts, I’d use a Stream.
Cheers.
Top comments (0)