State Object Controller topic

In the Fluttery Framework, the typical controller object extends the SateXController class and contains the business rules for the app. A controller is also used by a particular State object to deal with any event handling. The State object itself deals with just the interface. The State object would typically extend the StateX class, so to utilize the Controller object in its build() function or its other functions. Again, the controller is to work with the app's business rules and or address any events like the pushing of a button.

Contents
External State Control No Controller Bloat Control The State The Singleton Pattern The List of Device and System Events

External State Control

When a StateX object takes in a StateXController object through its constructor or though one of its add() functions (see screenshot), that controller now has access to that State object and all its properties and functions. That very fact allows for some powerful capabilities. Essentially, you now have the ability to call the State object's setState() function from outside its class---through that controller object! The basic requirement of any State Management in Flutter is to reliably call the setState() function of a specific State object. Controllers makes this easily possible.

page_01.dart

The two screenshots below demonstrate how easily you can now reference a particular State object from yet another State object if need be. The code below is from the example app that accompanies the Fluttery Framework package. The first screenshot is of the State class, Page2State, which displays 'Page 2' of the Three-Page Counter Example app. In this app, you're able to increment the counter of even a neighboring page with a tap of a button.

The first screenshot shows how to increment the counter on 'Page 1' from the 'Page 2' screen. The second screenshot shows how to increment the counter on 'Page 2' from the 'Page 3' screen. A simple demonstration, but a spectacular one if you think about it! Notice it's the same controller class object (unimaginatively named Controller) being used, and it can reference all three State objects at the same time!

page_02.dart page_03.dart

During the course of a typical app, as the user progressively moves deeper, for example, into the app going from screen to screen, the StateXController object retains the sequence of State objects its been assigned to in turn. It's 'current' state object (its .state property) is always the one residing in the current screen. However, as you see in the screenshots above, the controller has the means to reference 'past' State objects from previous screens (ofState() and stateOf()). Of course, when the user retreats back to the original screen, the controller's 'current' state object property reflects that change accordingly.

Further, the StateX class has access to some 27 events functions. Thus, if a controller is registered with that State object, those events are delegated to that controller. The controller is to directly handle any such events while the State object is to be remain concerned with just the interface. This all follows the clean architecture paradigm.

No Controller Bloat

You're able to add as many controllers as you want to an individual StateX object. This prevents making your controller class too big to manage---bloating it with all the business rules required. Instead, you can break down the logic into manageable segments each representing a particular aspect of the app's business rules , in turn, each aspect could be represented by an individual controller class if applicable. Note, when a StateX object has a number of registered Controllers, and a system event occurs, for example, those controllers will run in the order they were assigned to possibly address that event---if you've supplied the code to do so.

Of course, nothing is stopping you from having controllers call other controllers or other objects representing databases, or other third-party packages so to address the varying complexity of your app and its business rules. The 'controller side' of your app deals with the logic. You're free to organize the degree of abstraction and complexity necessary to do so leaving the interface to the StateX object.

page_01.dart

Control The State

Thus, when you register a controller to a StateX object, take advantage of the fact that a controller has all the functions like its corresponding StateX object. For example, if there's anything your controller may need to be initialized before a widget is displayed, remember a controller has its initState() function to initialize such requirements.

In the first screenshot below, the WordPairsTimer controller class has to set up its timer and pass its State object reference to the 'data object' called, model, before its particular State object can proceed to calling its build() function and display somthing on the screen. Since these are not operations pertaining to the interface directly, they are well-suited to reside in one of the designated controller objects.

The second screenshot below, is of another example app where it appears it's necessary for a controller to instantiate another controller called, ExampleAppController. It obviously a requirement and part of the 'business process' and so it too resides in a controller.

word_pair_timer.dart contacts_app.dart

Further note in the first screenshot above, the controller's corresponding deactivate() function turns off the timer if and when, in this instance, that screen is closed. But that's not sufficient. Let's assume this example app is running on a mobile phone. If and when the user chooses to answer a phone call, for example, and place this app in the background, again, it's good practice to turn off the timer. The timer is re-initialized only if and when the user returns to that app. This behavior is easily achieved in that controller as well using its didChangeAppLifecycleState() and resumedLifecycleState() functions to name a few. See below.

word_pair_timer.dart

The Singleton Pattern

I find the role of a Controller is best served by a single instance of that class throughout the life of the app. It's called upon to respond to various system and user events throughout an app's lifecycle and yet still retain an ongoing and reliable state. A factory constructor for the Controller class readily accomplishes this. See below.
class CounterController extends StateXController {
  factory CounterController() => _this ??= CounterController._();
  CounterController._() : super();
  static CounterController? _this;

With the Singleton pattern, making only one single instance of a particular class creates lesser overhead. Certainly not a steadfast rule, but it's suggested all controllers instantiate with a factory constructor. Again, doing so agrees with its general role as an ongoing custodian of the app's business rules and event handling. A clean, consistent, and manageable approach, and as it happens, one that adheres to good programming practices.

The Controller and State Events

As mentioned above, with the controller, you not only supply the app's business rules, but can also respond to the device and system events that commonly occur during an app's lifecycle. The sample code below lists all the available 'event' functions. In most cases, you'll only use the functions, initAsync(), initState(), deactivate(), and dispose() as well as its 'lifecycle' functions. However, they're all there for you as you may need them someday in a future app:

(Below is the Controller from the 'Three Page' Counter Example App.)
class CounterController extends StateXController {
  factory CounterController() => _this ??= CounterController._();
  CounterController._() : super();
  static CounterController? _this;

  /// The framework will call this method exactly once.
  /// Only when the [StateX] object is first created.
  @override
  void initState() {
    super.initState();
  }

  /// Called to complete any asynchronous operations.
  @override
  Future<bool> initAsync() async {
    return true;
  }

  /// The framework calls this method when the [StateX] object removed from widget tree.
  /// i.e. The screen is closed.
  @override
  void deactivate() {}

  /// Called when this State object was removed from widget tree for some reason
  /// Undo what was done when [deactivate] was called.
  @override
  void activate() {}

  /// The framework calls this method when this [StateX] object will never
  /// build again.
  /// Note: YOU DON'T KNOW WHEN THIS WILL RUN in the Framework.
  /// PERFORM ANY TIME-CRITICAL OPERATION IN deactivate() INSTEAD!
  @override
  void dispose() {
    super.dispose();
  }

  /// The application is not currently visible to the user, not responding to
  /// user input, and running in the background.
  @override
  void pausedLifecycleState() {}

  /// Called when app returns from the background
  @override
  void resumedLifecycleState() {}

  /// The application is in an inactive state and is not receiving user input.
  @override
  void inactiveLifecycleState() {}

  /// Either be in the progress of attaching when the engine is first initializing
  /// or after the view being destroyed due to a Navigator pop.
  @override
  void detachedLifecycleState() {}

  /// Override this method to respond when the [StatefulWidget] is recreated.
  @override
  void didUpdateWidget(StatefulWidget oldWidget) {}

  /// Called when this [StateX] object is first created immediately after [initState].
  /// Otherwise called only if this [State] object's Widget
  /// is a dependency of [InheritedWidget].
  @override
  void didChangeDependencies() {}

  /// Called whenever the application is reassembled during debugging, for
  /// example during hot reload.
  @override
  void reassemble() {}

  /// Called when the system tells the app to pop the current route.
  /// For example, on Android, this is called when the user presses
  /// the back button.
  @override
  Future<bool> didPopRoute() async {
    return super.didPopRoute();
  }

  /// Called when the host tells the app to push a new route onto the
  /// navigator.
  @override
  Future<bool> didPushRoute(String route) async {
    return super.didPushRoute(route);
  }

  /// Called when the host tells the application to push a new
  /// [RouteInformation] and a restoration state onto the router.
  @override
  Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
    return super.didPushRouteInformation(routeInformation);
  }

  /// The top route has been popped off, and this route shows up.
  @override
  void didPopNext() {}

  /// Called when this route has been pushed.
  @override
  void didPush() {}

  /// Called when this route has been popped off.
  @override
  void didPop() {}

  /// New route has been pushed, and this route is no longer visible.
  @override
  void didPushNext() {}

  /// Called when the application's dimensions change. For example,
  /// when a phone is rotated.
  @override
  void didChangeMetrics() {}

  /// Called when the platform's text scale factor changes.
  @override
  void didChangeTextScaleFactor() {}

  /// Brightness changed.
  @override
  void didChangePlatformBrightness() {}

  /// Called when the system tells the app that the user's locale has changed.
  @override
  void didChangeLocales(List<Locale>? locales) {}

  /// Called when the system is running low on memory.
  @override
  void didHaveMemoryPressure() {}

  /// Called when the system changes the set of active accessibility features.
  @override
  void didChangeAccessibilityFeatures() {}
}

The List of Device and System Events

(Tap on each below to see the source code and a further explanation of these function.)

deactivate activate didUpdateWidget didChangeDependencies reassemble didPopRoute didPushRoute didPushRouteInformation didChangeMetrics didChangeTextScaleFactor didChangePlatformBrightness didChangeLocales didChangeAppLifeCycle inactiveLifecycle pausedLifecycle detachedLifecycle resumedLifecycle didHaveMemoryPressure didChangeAccessibilityFeatures

Classes

AppController State Object Controller
A Controller for the 'app level'.