StateX

Greg Perry
9 min readAug 25, 2022

--

The long-awaited extension of the State class

When I first began learning Flutter back in 2018, I kept getting two particular warning messages. It was obvious I was doing something wrong and not taking full advantage of Flutter’s declarative approach to programming. These messages are conveyed in the two screenshots below:

‘@immutable’ warning
‘setState’ warning

The Warnings: ‘@immutable’ and ‘setState’

StatelessWidgets and StatefulWidgets are to contain unchanging (immutable) instance fields or properties — not doing so impedes Flutter’s functionality and degrades its overall performance. As for not calling the State class’ setState() function directly but through a subclass, that’s just good practice. It’s currently a ‘protected’ function — you’re to implement a subclass to call such a function.

When working with Flutter, you always need reliable access to a particular State object so to call its setState() function (or at least a StatefulElement’s markNeedsBuild() function). Doing so ‘rebuilds’ that portion of the screen involving that State object and reflects the changes made to the state object.

“ There is no imperative changing of the UI itself (like widget.setText)—you change the state, and the UI rebuilds from scratch.”

— flutter.dev Start thinking declaratively

Now all your ‘mutable’ code could go into the Dart file containing your State subclass. At first glance, that’s a good idea. Again, you always want ready access to the State object and its setState() function. However, that can make for a rather large and unmanageable Dart file. No, more often than not, placing such code in separate Dart files would be a better solution.

And so, I wrote the package state_extended with its class, StateX. Now, this is not to be confused with GetX. StateX involves extending the functions and features of the Sate class. As such, X stands for ‘extension,’ The StateX class contains 24 new functions compared to Flutter’s original State class. However, GetX and StateX 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. The difference here is that the StateX controllers can do anything its associated StateX object can do and more. This article will now introduce this.

I Like Screenshots. Tap Caption For Gists.

As always, I prefer using screenshots in my articles over gists to show concepts rather than just show code. I find them easier to work with frankly. However, you can click or tap on their captions to see the code in a gist or in Github. Tap or click on the screenshots themselves to zoom in on them for a closer look. With that, you ironically will find it best to read this article about mobile development on your computer rather than on your phone.

No Moving Pictures, No Social Media

There will be gif files in this article demonstrating aspects of the topic at hand. However, it’s said viewing such gif files is not possible when reading this article on platforms like Instagram or Facebook. They may come out as static pictures or simply blank placeholders. Please, be aware of this and maybe read this article on medium.com or on their own app.

Let’s begin.

Other Stories by Greg Perry

So again, when I began working with Flutter back in 2018, it was clear I needed a separate class representing the ‘Business Logic’ of the app but had ready access to the ever-important State object. The StateX class was the result.

“Ephemeral state (sometimes called UI state or local state) is the state you can neatly contain in a single widget.

Other parts of the widget tree seldom need to access this kind of state.
In other words, there is no need to use state management techniques (ScopedModel, Redux, etc.) on this kind of state. All you need is a StatefulWidget.”

— flutter.dev Ephemeral state

I would have written the last sentence a little more explicitly:
“All you need is a StatefulWidget with its State object retaining state.”

Show By Example

We’ll use the ol’ startup app created with every new Flutter project to demonstrate the StateX class in action. Here is a gist file for you to download and try yourself: statex_counter_app.dart. Below is a screenshot of this startup app. You can see there have been some changes made. The class, State, has been replaced with the class, StateX, and then that class object takes in a StateXController object named…well, ….Controller.

statex_counter_app.dart

As you now know, you should extend the State class to use it, and in this case, you have to extend the StateX class to use it. You’ll be glad you did —because it now includes a controller and much more capability as well as scalability.

In the next screenshot below, you can see highlighted with red arrows that the Controller object is referenced here and there in the State object’s build() function. By design, the controller is providing the data and the event handling necessary for the app to function correctly. However, do you see something missing in the screenshot? There’s no setState() function call?! It’s in the controller and called in the con.onPressed() function.

statex_counter_app.dart

Looking at the screenshot of the controller class below, you’ll find the setState() function call. Now, that’s a powerful capability! You’re in a class that can contain all the mutable properties and business logic you want as well as provide state management! It further has access to the State object’s own properties: widget, mounted, and context. Imagine what you’re controller can do with access to those properties as well.

statex_counter_app.dart

It’s easy enough to see when this link to the State object occurred. It happen when the StateX class was first instantiated. It’s there where the controller object was also instantiated and passed to the State object (see below). With that, the business logic component now has access to a State object and vice versa.

statex_counter_app.dart

As an aside, if you don’t want your ‘business logic’ dictating the ‘look and feel’ or your app’s interface, simply remove the setState() function call from the controller and return it back to the build() function (see below). Let the State object dictate when to call its setState() function instead. If you know me, you know I want developers to have options.

statex_counter_app.dart

Control The State

When using this package, through the course of an app’s lifecycle, a controller can be registered to any number of StateX objects. A StateXController object works with ‘the last’ State object it’s been assigned to but keeps a reference of any and all past StateX objects it’s worked with previously in the Widget tree. Very powerful.

When a screen closes (i.e. the current StateX object is disposed of), the controller focus points back to the previous StateX it was assigned to. This allows, for example, for one controller to sustain the app’s business logic for the duration of the running app while conveying that logic across any number of screens (i.e. any number of StateX objects).

‘First In, Last Out’ when queuing a controller’s State objects.

As it happens, in turn, a StateX object can have any number of controllers during the app’s lifecycle. Each controller could, for example, be dedicated to a particular responsibility independent of the other controllers or the rest of the app — encouraging more modular development of the software.

One State with many controllers

As a quick example, the screenshot of our simple example app below demonstrates some additional controller objects ‘linked’ to a StateX object.

statex_counter_app.dart

Xtend The State

Let’s finish up this article by simply listing the additional functions and features you gain when you use the StateX class. The first list below contains all functions unique to the StateX class. Most pertain to retrieving a particular controller currently linked to the StateX object. Later articles will demonstrate these functions in detail and note the examples on Github and Pub.dev.

add(StateXController? controller)
Add a specific StateXController to this State object.

addList(List<StateXController>? list)
Add a list of ‘Controllers’ to be associated with this StatX object.

StateXController? get controller
Provide the ‘first’ controller added to this State object
(allowed multiple controllers).

U? controllerByType<U extends StateXController>()
Retrieve a StateXController by type (allowed multiple controllers).

onError(FlutterErrorDetails details)
Its own Error Handler. The default routine is to dump the error to the console.

Find Your State

This next list is of functions found only in a StateXController object. Most here retrieve a particular StateX object currently running in the app. You’ll find this an indispensable capability.

addState(StateX? state)
Associate this StateXController to a specified StateX object.

StateX? get state
This getter returns the ‘current’ StateX object (can have many of them).

setState(VoidCallback fn)
Calls the ‘current’ StateX object’s
setState() function.

stateOf<T extends StatefulWidget>()
Retrieve a State object by its StatefulWidget (can have many of them).

ofState<T extends StateX>()
Retrieve a StateX object by type (can contain multiple State objects).

Share a Lifecycle Together

Lastly, there are twenty-one event handlers added to the StateX class in the state_extended package, and when a controller is linked to a StateX object, it too then has access to these event handlers and to events that may be triggered in the app. I suspect Android developers in particular will recognize this as an indispensable capability as well.

Init for One, Init for All

A screenshot of the StateX class and its initState() function below, presents how a StateX object, after running its own initState() function, then runs the initState() function of each and every controller currently associated with it at the time. You see, each controller may have its own operations or services that need to run before the StateX object can continue. Instead of a large and messy initState() function in the one State object — there can be individual controllers running their own initState() functions. Very clean. Very modular.

state_extended.dart

initState()
Called exactly once when the State object is first created.

initAsync()
Called exactly once at the app’s startup to initialize any ‘time-consuming’ operations that need to complete for the app can continue.

dispose()
When the State object will never build again. Its terminated.

didUpdateWidget(StatefulWidget oldWidget)
Override this method to respond when the State object’s accompanying StatefulWidget is destoryed and a new one recreated — a very common occurance in the life of a typical Flutter app.

didChangeDependencies()
When a dependency of this State object changes.

didChangePlatformBrightness()
Brightness changed.

didChangeLocale(Locale locale)
When the user’s locale has changed.

didChangeMetrics()
When the application’s dimensions change. (i.e. when a phone is rotated.)

reassemble()
Called during hot reload. (e.g. reassembled during debugging.)

didPopRoute()
Called when the system tells the app to pop the current route.

didPushRoute(String route)
Called when the app pushes a new route onto the navigator.

didPushRouteInformation(RouteInformation routeInformation)
Called when pushing a new RouteInformation and a restoration state
onto the router.

deactivate()
When the State object is removed from the Widget tree.

didChangeTextScaleFactor()
When the platform’s text scale factor changes.

didHaveMemoryPressure()
When the phone is running low on memory.

didChangeAccessibilityFeatures()
When the system changes the set of active accessibility features.

didChangeAppLifecycleState(AppLifecycleState state)
Called when the app’s in the background or returns to the foreground.
The four following functions use this one to address specific events.

inactiveLifecycleState()
The application is in an inactive state and is not receiving user input.

pausedLifecycleState()
The application is not currently visible to the user, not responding to
user input, and running in the background.

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

resumedLifecycleState()
The application is visible and responding to user input.

StateX Series

You can continue from this intro. to the 4-part series of articles below that even further examines the functions and features of the state_extended package. Each will detail one of the four example apps that accompany the package when it’s installed.

--

--