AppStateX class topic

The App's State Object

The AppStateX class is designed to be the first state object for your app. For this role, it has some unique features. Note the video to the right and the series of changes being made. The app's overall appearances is being changed with just a tap. You can turn off the 'debug banner' for example. You're able to change the color, the font, and even the interface design from Material to Cupertino and back again. You're even able to use some of Flutter's development tools right there at runtime. You can turn on a graph conveying the general performance of the running app, highlight the widgets and other components that make up the presented screen, and supply an grid over the app's whole interface. All on a whim. You can even switch back and forth the Material design from version 2 to version 3.

Now, those series of changes doesn't involve the home screen. What I mean is, as simply as it is, (this app might be too simple an example) the string,'You have pushed the button this many times:', isn't rebuilt with every change. By design, the count is rebuilt with every change and with every tap of the button of course, but if the screen was a little more complicated with a few more widgets, in most cases, they would be left untouched like that lone Text widget containing the String (see second screenshot). That makes for an efficient app. The less an app's interface is updated, the better the performance they always say.

Flutter does use the declarative approach rebuilding the whole screen from scratch at times, however in this example app, there are two State objects retaining separate states. The home page displaying the count and that String in the center of the screen has the State class, _MyHomePageState, while the rest of the screen containing the title bar, the popup menu, the drawer, and the floating action button (when the Scaffold widget is being used) is all managed by the State class, _MyAppState.

Contents
Control Preferences Fonts Get Control Development Parameters New Face New Font Errors

The example app being examined in this topic can be found in the repository, appstatex_example_app, and is available for you to download. As previously stated, this app involves two particular State objects. Again, one deals with the 'current' screen being displayed. The other deals with the rest of the app as a whole. The point is, you will find it's very advantageous to have this separate 'App State' object dealing with the overall 'look and feel' of the app. Each subsequent screen displayed then has its own State object. The State class, _MyAppState, is having its setState() function called with every tap of a Switch widget for a development tool or with every tap of a menu option leaving the 'current screen' untouched.

The first screenshot below is of the State class,_MyAppState. As you see, it extends the class, AppStateX, which in turn, extends the StateX class. Being the 'base' or 'root' of the app, it's here where a developer would traditionally get things up and running: opening databases, web services, etc. In particular, as you see highlighted in the first screenshot below, it's here where all the necessary controllers that deal with the app's business rules are instantiated. All the code necessary to run the app as intended is made accessible in instance variables or taken in by the State object, _MyAppState.

myapp_view.dart

Control

At a glance, you can deduce each controller class has a specific responsibility; a particular role to play in the running of this app. When instantiated, you can guess they're performing some initiating operation, or simply making their code available to the app as is the case of the three highlighted in the second screenshot above.

Let's take a look at the 'App State' Object's own controller called, MyAppController. In the first screenshot below, it's passed to the constructor using the named parameter, controller. It could have just as easily been passed in the List using the named parameter, controllers, but this is the State object's 'main' controller assigned to the State object's controller property.

In the second screenshot, you can see it has a specific role in the app. It is to determine which interface (Material or Cupertino) is to be display at start up. You can see this controller has its own initAsync() function implemented so to assign the appropriate design. If you've read Get started, you're aware the initAsync() function in both the State and controller objects serves to complete any necessary asynchronous operations before the State object calls its build() function. And so, before this first screen appears, this example app is going to know which interface design to use.

myapp_view.dart myapp_controller.dart

Preferences

The static function, getBool(), is called to determine the design and is found in the Prefs class. As it happens, the Prefs class is another controller (extends StateXController) and is used throughout the app so to readily access the popular shared_preferences plugin. There's no rule controllers can't call other controllers. In fact, it's most advantageous to be able to easily do so. That simple fact makes for a clean architecture both scalable and adaptive. The boolean instance variable, useMaterial, is assigned a saved value. It otherwise defaults to the value, true.

In the first screenshot below, we see the Prefs class is instantiated and also passed to the App's State object. Doing so, gives it access to the State object, and its lifecycle. That means its initAsync() function is called after the one in the controller, MyAppController. It's there in its function where the shared_preferences plugin is initialized and readied. See how things are coming along? A pretty uniformed and consistent approach is made available to you when it comes to starting up your app. Granted, in this case, the Prefs class is accessed by the MyAppController class before its initAsync() is called, but that's an exception. That's why the static function, getBoolF(), was used. Regardless, you'll see things are being set up in a consistent fashion.

myapp_view.dart preference_controller.dart

Fonts

Next one is the GoogleFontsController. In the first screenshot below, you see it taken in by the App's State object. In the second screenshot, you see its initAsync() function loading the specified Google fonts. They are the fonts available to you when you want to change the app's font. Involved is an asynchronous process of loading them all in. Again, this is conveniently accomplished with the consistent approach of using a controller, and its initAsync() function. It's called after the initAsync() function from the Prefs class. While this is all being done, a black screen is presented to the user with a circular spinner displayed in the center. Optionally, you could have a 'splash screen' displayed instead.
myapp_view.dart google_fonts.dart

Get Control

Ok, there's clearly a separation of responsibility here. Separate blocks of code (controllers) with ready access to State objects. One deals with changing the app's font, one works the color of the app, and one changes the interface design. There's one that works with presenting the development tools, there's one that involves the counter in the center of the screen, and, of course, there's one that works with the app overall.

Let's continue with the _MyAppState class and look further down at its initState() function (see the second screenshot below). The property variable, googleFonts, is assigned an instance of the GoogleFontsController class. Note, in the first screenshot, that class was instantiated and passed to the State object. Because of this, the controllerByType() function can be used to retrieve that instance of the class. However, a factory constructor was used in this class, and so the property variable can just as well be assigned by another constructor call as it will be the same instance (see the second screenshot below). Again, not a hardened rule, but the Singleton pattern appears to be most suitable for State Object Controllers because of the role they play. A single instance for the duration of a running app as a representative of the app's business rules and event handling has proven to be most effective. Regardless, the property variable, googleFonts, is assigned and ready for use.

myapp_view.dart myapp_view.dart

Development

On the other hand, the property variable, dev, was assigned right away with the App state's constructor. It's used to convey any selected development tool if and when any of the class, DevTools, own properties is set to true (see the second screenshot below). Also in the first screenshot below, you can see the App State's controller assigned to the variable, app. It's also seen in the second screenshot determining which interface design to be displayed using its property, useMaterial.

Do you see the separation of work here? The State object's 'build' function determines how things are displayed while its controllers determine what things are displayed. In many cases, the State object's build function will be dotted with an assortment of its controller objects performing the event handling and business rules receptive by that particular screen.

myapp_view.dart myapp_view.dart

Parameters

Now we're going to take a look at what's inside the _materialView() and _cupertinoView() functions. Of course, one utilizes the Material interface design, the other uses widgets for the Cupertino design. That means one uses the widget, MaterialApp, the other uses the widget, CupertinoApp (see below). Highlighted in the screenshots below are the controller variables and their properties. Conceivably, you could have a variable assigned to all 34 named parameters for the MaterialApp widget ---all 27 parameters for the CupertinoApp widget. Call the setState() function for the AppStateX object, and you'll have a different looking app.

myapp_view.dart myapp_view.dart

New Face

When the interface design is switched from Material to Cupertino (see video), all the business involved to make this happen can be found in the controller, MyAppController. You'll find even the popup menu is defined in this controller. Since the popup menu was so simple, I chose to place the popup menu code in the App's controller.

In the first screenshot below, you see the controller's useMaterial property determines the appropriate menu option depending on the current interface. Remember, this controller has access to its State object (_MyAppState) through the property, state. It is readily utilized here. For example, if the Cupertino interface is being used, there's no need for the color picker menu option:

if (!(state?.usingCupertino ?? false))

What's interesting is the second screenshot. We're still in the controller, MyAppController, with its access to the State object, MyAppState. Again, that's the 'root' or first State object, and so calling the setState() function would convey the new interface design. However, it also has the property, rootState. In the second screenshot, we see that property is used instead of the state property achieving the same outcome. That's because all controllers and State objects have access to the 'App State object' because all have the property, rootState. Easy access to the App's 'first' State object will prove to be a most powerful capability. Lastly, note the static function,Prefs.setBool('useMaterial', false), records false so that, next time the app starts up, the Cupertino interface design is used.

myapp_controller.dart myapp_controller.dart

And so, as you see in the first screenshot below, when either the setState() or the rootState() function is called, the App State object's build() function will execute again and the useMatrial property determines the appropriate interface.

Now, in the video below, we're repeatedly changing the app's font with no trouble at all. We'll see how that's done next.

myapp_view.dart

New Font

Let's walk with the controller responsible for supplying and changing the app's font. It's the GoogleFontsController and, in the first screenshot below, it's instantiated to the App's State object, _MyAppState. As you know, it's then assigned to the instance variable, googleFonts, in the State object's initState() function (see the second screenshot below). That variable is now available to the rest of the State object and its functions.

myapp_view.dart myapp_view.dart

For example, further down the State object in its _materialView() function, that variable is then utilized to assign the font to be used by the app. The GoogleFontsController is displayed in the second screenshot below. The stretch of code presented there assigns a new font to the app. You can see it's assigned to the 'current font' variable, _font. It's then recorded as the default font, and then the App State object's is 'refreshed' using the property, rootState. That means the code in the first screenshot runs again with the property, googleFonts?.font, supplying the new font. As easy as all that.

myapp_view.dart google_fonts.dart

Errors

Finally, let's introduce what may be the most important aspect of the AppStateX class: it's error handling capability. Handle your errors. Your app will have errors and exceptions, and you should anticipate them. Flutter actually makes that a distinction with two types of class objects. An Error object represents a program failure that the programmer should have avoided, while an Exception object is intended to be caught and contains useful data fields to help you to at least close down resources, notify developers, and fail gracefully.

Every StateX class has an onError() function for you to implement and handle any exceptions that may occur while that State object is running. The AppStateX class, however, implements its own onError() function to call the onError() function of the 'last' State object that was instantiated when the exception occurred. It's a simple, but a powerful arrangement. The AppStateX class records Flutter's current error handler and explicitly assigns this approach instead. Before the AppStateX class then terminates, it returns the original error handler. Note, there's also an onAsyncError() function for errors occurring while the State object was starting up. We'll demonstrate these functions next, and even introduce a custom ErrorWidget builder to display a widget when a State object fails to do so.

Such error handling will allow you to possibly recover from the exception and allow the app to continue as intended. For example, in this very very simple app there's been a exception occurring with every tap of the count button. It would normally result in the count not incrementing at all, but the particular exception is identified, and the onError() routine in the controller itself knows what to do to address the issue.

The first screenshot has the exception invoked in the controller's onPressed() function. In the second screenshot, the onError() function is called, and the exception is handled. The count is successfully incremented and displayed.

homepage_controller.dart homepage_controller.dart

The first screenshot below, shows the onError() and onAsyncError() functions for the State class, _MyHomePageState. Again, the _MyHomePageState State object delegates any errors to its controller, MyHomePageController, while its onAsyncError() function, in this example, merely prints to the console when triggered by an error. In the second screenshot, you see how that particular error is triggered. When the State object is starting up and something goes wrong in its initAsync() function or in the initAsync() function of one of its controllers, its onAsyncError() function is called as a means for the developer to 'clean things up' before the app fails. See how that works?

homepage_view.dart homepage_controller.dart

The AppStateX class is displayed in the first screenshot. Its buildIn() function is presented, and its controller has the flag, errorInBuild, to cause an error right in the State object's 'build' function. That means the intended widget is not displayed. If this error occurs during development, as you know, a red screen is displayed. However, in production, such an error will produce an 'ugly' gray screen. Of course, you can always use your own low-level screen instead to display such errors. The MyAppController's constructor is in the second screenshot. Highlighted is the line below replacing the red screen with the custom widget from the class, AppWidgetErrorDisplayed:

ErrorWidget.builder = (details) => AppWidgetErrorDisplayed(handler: this, ...

myapp_view.dart myapp_controller.dart

Instead of the unimaginative red screen, you get a screen a little easier read (see below).
Take a copy of the file, error_widget.dart, and change it for you're own use.

Classes

AppStateX<T extends StatefulWidget> Get started StateX class AppStateX class
The StateX object at the 'app level.' Used to effect the whole app by being the 'root' of first State object instantiated.
SetState AppStateX class
Used like the function, setState(), to 'spontaneously' call build() functions here and there in your app. Much like the Scoped Model's ScopedModelDescendant() class. This class object will only rebuild if the App's InheritedWidget notifies it as it is a dependency.