state_extended 1.1.0 icon indicating copy to clipboard operation
state_extended: ^1.1.0 copied to clipboard

The State Class is extended with State object controllers and lifecycle events.

StateX #

codecov CI Medium Pub.dev GitHub stars Last Commit

statex

An Extension of the State class #

This package expands the capabilities of Flutter's State class. This fundamental component of Flutter's state management had room for improvement. The capabilities of Flutter's State class now includes 'State Object Controllers' and the app's 'lifecycle events.'

StateX should not be confused with GetX, however, they do have their similarities. In particular, both involve 'controllers' that generally contain the 'business logic' involved in any given app. GetX has its GetxController class while StateX has its StateXController class.

Installing #

I don't always like the version number suggested in the 'Installing' page. Instead, always go up to the 'major' semantic version number when installing my library packages. This means always entering a version number trailing with two zero, '.0.0'. This allows you to take in any 'minor' versions introducing new features as well as any 'patch' versions that involves bugfixes. Semantic version numbers are always in this format: major.minor.patch.

  1. patch - I've made bugfixes
  2. minor - I've introduced new features
  3. major - I've essentially made a new app. It's broken backwards-compatibility and has a completely new user experience. You won't get this version until you increment the major number in the pubspec.yaml file.

And so, in this case, add this to your package's pubspec.yaml file instead:

dependencies:
  state_extended:^1.0.0

Documentation #

Turn to this free Medium article for a full overview of the package plus examples: StateX

Example Code #

Further examples can be found in its Github repository: example app

//
import 'package:state_extended/state_extended.dart';

import 'package:flutter/material.dart';

void main() => runApp(const MyApp(key: Key('MyApp')));

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

  /// This is the App's State object
  @override
  State createState() => _MyAppState();
}

class _MyAppState extends AppStateX<MyApp> {
  factory _MyAppState() => _this ??= _MyAppState._();
  static _MyAppState? _this;

  _MyAppState._()
      : super(
          controller: AppController(),
          object: 'Hello',
        );

  /// Override this build function if you don't want to use the built-in FutureBuilder or InheritedWidget
  @override
  Widget build(BuildContext context) => super.build(context);

  /// This is the widget returned by the built-in FutureBuilder widget.
  /// Override this build function if you don't want to use the built-in InheritedWidget
  @override
  Widget buildWidget(BuildContext context) => MaterialApp(
        home: super.buildWidget(context),
      );

  /// This returns the 'child' widget supplied to the InheritedWidget.
  @override
  Widget buildChild(BuildContext context) => MyHomePage(key: UniqueKey());
}

/// A Controller at the 'app level'
class AppController extends StateXController {
  /// Used for long asynchronous operations that need to be done
  /// before the app can be fully available to the user.
  /// e.g. Opening Databases, accessing Web servers, etc.
  @override
  Future<bool> initAsync() async {
    final init = super.initAsync();
    return init;
  }

  /// Supply an 'error handler' routine if something goes wrong
  /// in initAsync() routine above.
  @override
  bool onAsyncError(FlutterErrorDetails details) => false;

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

  /// The framework calls this method whenever it removes this [StateX] object
  /// from the tree.
  @override
  void deactivate() {
    super.deactivate();
  }

  /// Called when this object is reinserted into the tree after having been
  /// removed via [deactivate].
  @override
  void activate() {
    super.activate();
  }

  /// The framework calls this method when this [StateX] object will never
  /// build again.
  /// Note: THERE IS NO GUARANTEE THIS METHOD WILL RUN in the Framework.
  @override
  void dispose() {
    super.dispose();
  }
}

/// The Home page
class MyHomePage extends StatefulWidget {
  const MyHomePage({
    Key? key,
    this.title = 'Flutter Demo',
  }) : super(key: key);

  // Fields in a StatefulWidget should always be "final".
  final String title;

  @override
  State createState() => _MyHomePageState();
}

/// This 'MVC version' is a subclass of the State class.
/// This version is linked to the App's lifecycle using [WidgetsBindingObserver]
class _MyHomePageState extends StateX<MyHomePage> {
  /// Let the 'business logic' run in a Controller
  _MyHomePageState() : super(Controller()) {
    /// Acquire a reference to the passed Controller.
    con = controller as Controller;
  }
  late Controller con;

  @override
  void initState() {
    /// Look inside the parent function and see it calls
    /// all it's Controllers if any.
    super.initState();

    /// Retrieve the 'app level' State object
    appState = rootState!;

    /// You're able to retrieve the Controller(s) from other State objects.
    var con = appState.controller;

    con = appState.controllerByType<AppController>();

    con = appState.controllerById(con?.identifier);
  }

  late AppStateX appState;

  /// This is 'the View'; the interface of the home page.
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              /// Display the App's data object if it has something to display
              if (con.dataObject != null && con.dataObject is String)
                Padding(
                  padding: const EdgeInsets.all(30),
                  child: Text(
                    con.dataObject as String,
                    key: const Key('greetings'),
                    style: TextStyle(
                      color: Colors.red,
                      fontSize: Theme.of(context).textTheme.headline4!.fontSize,
                    ),
                  ),
                ),
              Text(
                'You have pushed the button this many times:',
                style: Theme.of(context).textTheme.bodyText2,
              ),

              /// Linked to the State object's built-in InheritedWidget.
              SetState(
                builder: (context, dataObject) => Text(
                  '${con.count}',
                  style: Theme.of(context).textTheme.headline4,
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          key: const Key('+'),

          /// rebuilds only the Text widget containing the counter.
          onPressed: () => con.onPressed(),
          child: const Icon(Icons.add),
        ),
      );

  /// Supply an error handler for Unit Testing.
  @override
  void onError(FlutterErrorDetails details) {
    /// Error is now handled.
    super.onError(details);
  }
}

/// Everything a State object can do, this Controller can do as well!
class Controller extends StateXController {
  factory Controller([StateX? state]) => _this ??= Controller._(state);
  Controller._(StateX? state)
      : _model = Model(),
        super(state);
  static Controller? _this;

  final Model _model;

  /// Note, the count comes from a separate class, _Model.
  int get count => _model.integer;

  // The Controller knows how to 'talk to' the Model and to the View (interface).
  void onPressed() {
    //
    _model.incrementCounter();

    /// Calls only 'SetState' widgets
    /// or widgets that called the dependOnInheritedWidget(context) function
    notifyClients();

    /// Retrieve a particular State object.
    final homeState = stateOf<MyHomePage>();

    /// If working with a particular State object and if divisible by 5
    if (homeState != null && _model.integer % 5 == 0) {
      //
      dataObject = _model.sayHello();
      setState(() {});
    }
  }

  /// Used for long asynchronous operations that need to be done
  /// before the app can be fully available to the user.
  /// e.g. Opening Databases, accessing Web servers, etc.
  @override
  Future<bool> initAsync() async {
    final init = super.initAsync();
    return init;
  }

  /// Supply an 'error handler' routine if something goes wrong
  /// in initAsync() routine above.
  @override
  bool onAsyncError(FlutterErrorDetails details) => false;

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

    /// Access to it's corresponding State object
    final statefulWidget = state?.widget;

    assert(statefulWidget is MyHomePage,
        'Of course the Controller has access to the StatefulWidget');

    /// A State object can reference it's 'current' State object controller.
    var thisController = state?.controller;

    /// The same controller can be retrieved by type if you know the type
    /// Note, it has to be a Controller explicitly added to the State object at some time.
    thisController = state?.controllerByType<Controller>();

    /// The same controller can be retrieved by its unique identifier if you know it.
    /// You then don't have to know the type or the type is private with a leading underscore.
    /// Note, it has to be a Controller explicitly added to the State object at some time.
    thisController = state?.controllerById(thisController?.identifier);

    assert(thisController == this,
        'Just demonstrating the means to retrieve a Controller.');

    /// You can retrieve a Controller's state object by its StatefulWidget
    /// Good if the state class type is unknown or private with a leading underscore.
    var stateObj = stateOf<MyHomePage>();

    assert(stateObj == state, 'They should be the same State object.');

    /// Retrieve the 'app level' State object
    var appState = rootState;

    assert(appState is _MyAppState,
        "Every Controller has access to the 'first' State object.");

    /// The 'app level' State object has *all* the Stat objects running in the App
    /// at any one point of time.
    stateObj = appState?.stateByType<_MyHomePageState>();

    assert(stateObj == state, 'They should be the same State object.');

    /// Retrieve the State object's controller.
    var con = appState?.controller;

    /// You're able to retrieve the Controller(s) from other State objects.
    /// if you know their type
    con = appState?.controllerByType<AppController>();

    /// You're able to retrieve the Controller(s) from other State objects.
    /// if you know their unique identifier.
    con = appState?.controllerById(con?.identifier);

    /// If you know the type, every State object can access any Controller
    var appController = state?.controllerByType<AppController>();

    assert(appController == con, 'They should be the same object.');

    assert(dataObject == 'Hello', 'The data object passed to the App.');
  }

  /// The framework calls this method whenever it removes this [StateX] object
  /// from the tree.
  @override
  void deactivate() {
    super.deactivate();
  }

  /// Called when this object is reinserted into the tree after having been
  /// removed via [deactivate].
  @override
  void activate() {
    super.activate();
  }

  /// The framework calls this method when this [StateX] object will never
  /// build again.
  /// Note: THERE IS NO GUARANTEE THIS METHOD WILL RUN in the Framework.
  @override
  void dispose() {
    super.dispose();
  }

  /// Called when the corresponding [StatefulWidget] is recreated.
  @override
  void didUpdateWidget(StatefulWidget oldWidget) {
    /// The framework always calls build() after calling [didUpdateWidget], which
    /// means any calls to [setState] in [didUpdateWidget] are redundant.
    super.didUpdateWidget(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() {
    return super.didChangeDependencies();
  }

  /// Called whenever the application is reassembled during debugging, for
  /// example during hot reload.
  @override
  void reassemble() {
    return super.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.
  ///
  /// Observers are notified in registration order until one returns
  /// true. If none return true, the application quits.
  /// This method exposes the `popRoute` notification from
  // ignore: comment_references
  /// [SystemChannels.navigation].
  @override
  Future<bool> didPopRoute() async {
    return super.didPopRoute();
  }

  /// Called when the host tells the app to push a new route onto the
  /// navigator.
  /// This method exposes the `pushRoute` notification from
  // ignore: comment_references
  /// [SystemChannels.navigation].
  @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.
  /// This method exposes the `popRoute` notification from
  // ignore: comment_references
  /// [SystemChannels.navigation].
  ///
  /// The default implementation is to call the [didPushRoute] directly with the
  /// [RouteInformation.location].
  @override
  Future<bool> didPushRouteInformation(RouteInformation routeInformation) {
    return super.didPushRouteInformation(routeInformation);
  }

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

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

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

  /// Called when the system tells the app that the user's locale has changed.
  @override
  void didChangeLocale(Locale locale) {
    didChangeLocale(locale);
  }

  /// Called when the system puts the app in the background or returns the app to the foreground.
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    /// Passing these possible values:
    /// AppLifecycleState.inactive (may be paused at any time)
    /// AppLifecycleState.paused (may enter the suspending state at any time)
    /// AppLifecycleState.detach
    /// AppLifecycleState.resumed
    super.didChangeAppLifecycleState(state);
  }

  /// The application is in an inactive state and is not receiving user input.
  ///
  /// On iOS, this state corresponds to an app or the Flutter host view running
  /// in the foreground inactive state. Apps transition to this state when in
  /// a phone call, responding to a TouchID request, when entering the app
  /// switcher or the control center, or when the UIViewController hosting the
  /// Flutter app is transitioning.
  ///
  /// On Android, this corresponds to an app or the Flutter host view running
  /// in the foreground inactive state.  Apps transition to this state when
  /// another activity is focused, such as a split-screen app, a phone call,
  /// a picture-in-picture app, a system dialog, or another window.
  ///
  /// Apps in this state should assume that they may be [pausedLifecycleState] at any time.
  @override
  void inactiveLifecycleState() {
    super.inactiveLifecycleState();
  }

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

  /// 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() {
    super.detachedLifecycleState();
  }

  /// The application is visible and responding to user input.
  @override
  void resumedLifecycleState() {
    super.resumedLifecycleState();
  }

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

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

/// This example has a separate class that contains the data.
class Model {
  int get integer => _integer;
  int _integer = 0;

  int incrementCounter() => ++_integer;

  String sayHello() => 'Hello There!';
}
1
likes
140
pub points
64%
popularity

Publisher

verified publisher iconandrioussolutions.com

The State Class is extended with State object controllers and lifecycle events.

Homepage
Repository (GitHub)

Documentation

API reference

License

Icon for licenses.BSD-2-Clause (LICENSE)

Dependencies

flutter, flutter_test, universal_platform

More

Packages that depend on state_extended