DEV Community

Cover image for StateX
Greg Perry
Greg Perry

Posted on • Edited on

StateX

Flutter’s own State Management made better

Mike Rydstrom continues to post on Twitter the top State Management Solutions currently used in the industry. Below is the latest Top 10 dated, October 7th, 2022.

TOP 10 State Management Solutions by Likes

The last article I published showcased my Fluttery Framework. It too began listing the previous version of this list posted by Mike Rydstrom. It was in that article where it appeared I was comparing my Fluttery Framework to those State Management solutions when, of course, that was unfair. It’s more than a State management solution. It’s a whole framework used to reliably expedite your next Flutter app development. I did admit my subterfuge by the end of the article, but a subsequent Reddit comment did highlight the confusion.

The Fluttery Framework

No, if there is to be a comparison, it should instead involve the state_extended package — used by the Fluttery Framework. Although Flutter, in my opinion, is more than up to the task, this state_extended package merely enhances Flutter’s approach to State Management. More specifically, it extends the capabilities of Flutter’s own State class with the StateX class.

With this article, I’ll review some of the highlights of this class. My mantra while writing it was always, ‘Keep It Flutter.’ This was for making Flutter apps after all. The State class is Flutter’s main player in State Management, and I decided to stay focused on that class. The solution should look like Flutter, and it should act like Flutter.

I’m a Flutter developer now. Not a web developer, not an Android developer, or a .Net developer. Thus, I chose not to carry over any of the design patterns or techniques prevalent in those past platforms or tools. Google engineers have made a distinct cross-platform software development kit that transcends the current platforms of the day allowing Flutter apps to run on Android, iOS, Windows, Linux, and the Web.

Keep it Flutter.

I‘ve thrown my hat in the ring of State Management solutions. I do find it more intuitive as it more resembles Flutter's own approach to State Management. Am I conveying any aspirations with this entry? No. No one solution is sufficient in software development. It’s just another approach available to you for your consideration...

Above the Law (1988)

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. If reading on your phone, use two fingers to expand and collapse the image. Ironically, you may 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.

Show By Example

Let’s introduce this class using a video running below. It presents one of the example apps that accompanies the state_extended package. It demonstrates the ready access you have to any particular State object in your app so as to execute its vital setState() function and update the app’s interface. Being such a fundamental requirement in Flutter, again whole architectures (Provider, Riverpod, GetX, etc.) have been built just for that very purpose.

Watching the video, you can see there are not one but three counter pages in this example, and the user can easily increment the counter of a previous page from a more advanced page with a tap of a button. Further, you’ll find, unlike Page 3, Page 2 is able to retain its count even when the user retreats all the way back to Page 1 and then back to Page 2. How is that possible? Finally, back on Page 3, you’re able to reset the count on Page 1 back to zero — again with a simple tap.

state_extended.dart

Now all this may appear to be pretty mundane abilities, but it requires this package to safely and reliably access State objects situated in other parts of your app! Specifically, other parts of the Widget tree. Again, the other architectures in that list above all strive to attain this same capability. You see, by design, Flutter does not allow ready access to State objects as they’re deemed critical to retaining state. After this article, you’re going to see how it’s done safely and reliably.

Why Extend State?

So again, I wrote this package because I wanted access to any particular State object. I could then call its setState() function any time, anywhere I wanted. However, that wasn’t the very first reason I built this package. Back in March of 2018, when I was first learning Flutter, it took time to understand how to use widgets properly. As you know, a State class accompanies a StatefulWidget and contains the build() function. It’s that function that essentially returns the app’s interface, and so I felt when implementing a State class, it should only contain code regarding the app’s interface — adhering to the characteristics of a clean architecture.

As an advocate for such an architecture, I regularly separate an app’s code into three areas of responsibility: its interface, its data source, and its event handling. This approach has proven time and time again to alleviate the complexities of software development. I quickly discovered when first learning Flutter that keeping all the code that makes up an app inside a State class would make things unnecessarily tedious. There was a lot of scrolling up and down the soon-to-be immense streams of code in that one Dart file. Not good.

I had to decide where the app’s event handling and business logic should reside. The StatefulWidget class, of course, was out of the question. Also by design, a StatefulWidget is created, destroyed, and recreated again and again during the lifetime of a typical Flutter app. All that should really be in a StatefulWidget is its createState() function. Any additional code should be immutable otherwise performance is affected.

Thus, when you first started learning Flutter, you no doubt encountered that warning essentially saying: “This class is marked as ‘@immutable’, but one or more of its instance fields aren’t final.” Below is an example of this with the keyword, final, removed from the instance field, title, in the StatefulWidget, MyHomePage.

my_home_page.dart

Do yourself a favor, and don’t ignore such warnings. Such ‘mutable’ properties in a StatefulWidgat are just taking up memory, using up CPU cycles, and only impeding performance. I finally concluded, this called for a separate Dart file. A separate file or at least a separate class that is the controller for the State class.

Flutter’s StatefulWidget vs. StateX’s StatefulWidget

This concept is not unknown to Flutter. I suspect you’ve already encountered a number of controllers while working with Flutter’s own widgets— many literally have named parameters called ‘controller.’ And so there it was. The State class would be concerned with the interface, and the State Object Controller class would be concerned with the event handling and logic. A distinct separation that has proven very beneficial while ‘keeping it Flutter.’

The Cycle Of Life

Another reason for this package was due to the fact I was coming from the Android world. I had grown accustomed to working with the ‘life-cycle’ events that commonly occur in Android apps, and I soon discovered implementing such a means in Flutter apps involved the WidgetObserver class using the command: WidgetsBinding.instance!.addObserver(this). With that, my State objects would then have the didChangeAppLifecycleState() function, and can then readily work with a Flutter app’s own life cycle — an important capability on any device or platform.

Android’s lifecycle events

Flutter’s lifecycle events in StateX

With that, you’re now privy to a number of system events that may potentially affect your app. They’re also readily accessible to the State Object Controllers. In both this State class and Controller class, these system events are addressed by a series of functions — all are listed below. I suspect you’ll readily recognize some of them or at least appreciate their importance.

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

Access The State

The functions listed below are also found in these State Object Controllers and pertain to getting ready access to State objects so coveted by those other State Management solutions. Indeed, it is a powerful capability.

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

state_extended.dart

Gain Context

Having worked in Flutter for a time, you may now know that retrieving the BuildContext object called, context, can be a hassle! Some avoid the effort altogether and gravitate to packages that seemingly relieve you of the task (e.g. go_router). Well, no problem. Here, you can either use a StateX object or a controller to get ‘the latest’ context with the following property:

state_extended.dart

The Root Of It All

Flutter is made up of trees. There’s the Widget tree, the Element tree, and the Render tree. And it all starts at the root — the first widget. And you’ll find access to the ‘first’ State object is a valuable capability — particularly if it’s the special ‘App’ State and represents the beginning of your app’s widget tree. Again, there’s a property for that in both the StateX and Controller classes.

state_extended.dart

The State Of Control

And so, with a SOC (State Object Controller) passed to a StateX object, you’re then able to call that State object’s setState() function from outside its very class — a very powerful capability indeed. For example, in the first screenshot below, you see the State object for the ‘Page 1’ StatefulWidget is taking in a controller object (i.e. the class, Controller, is instantiated and passed to the Page1State constructor). Tap and zoom in on the screenshots to get a better look.

page_01.dart

page_01.dart

In the second screenshot above, you see highlighted, a reference to that controller calling its setState() function. It’s called when you’re on Page 1 and tapping on its plus-sign button. As you see in the video, the counter increments with every tap (Please, don’t mind the black screen with the spinning circular indicator. That’s for another feature to be described later). Note, if you were to comment out that setState() command, of course, those increments will no longer be displayed. That’s because you’re no longer calling the State object’s setState() function. There’s no State Management. Follow me so far?

Now watch the video closely (tap to zoom in) and see the counter on Page 1 is also being incremented even when you’re on Page 2! Of course, that means the setState() function for Page 1’s State object is being called somewhere. It means you’ve access to the State object, Page1State, somehow. Do you know how? You guessed it, it’s through the controller object, Controller. We’ll see how this is done next.

Below is the screenshot of the State object, Page2State, used on Page 2. Note, it too is taking in the Controller class at its constructor. Further, it’s assigning it to the property, con. Right now, however, we’re interested in how the counter on Page 1 is incremented here on Page 2. In the second screenshot below, you can see how this is done in that State class’ onPressed() function.

page_02.dart

page_02.dart

The onPressed() function references the controller again, and it is used to retrieve the State object, Page1State. Since Page 1’s State object has the public property, count, it can be incremented here. Highlighted in the second screenshot above are the controller’s two functions that allow you to retrieve a State object. One does so by type (ofState()) while the other does so by referencing the State object’s own StatefulWidget (stateOf()). And so, that’s how the incrementing is done on another page. These controllers can do a lot for you!

Now, the interesting thing to note at this point is that the same controller object, Controller, is also being taken in by the second page‘s State object, Page2State. And so, when you tap on the plus-sign button on the second page, you’re again calling that controller’s setState() function. However, it now calls the setState() function for the State object, Page2State!

Further still, as you see in the video above when returning to Page 1, the Controller also returns to the State object, Page1State, and therefore, with every tap of the plus-sign button, calls the appropriate setState() function. Thus, these controllers can work up and down the widget tree serving the ‘current’ State object! Very nice indeed.

Now, let’s take a look at how the counter is incremented on Page 2. It’s done differently — in a fashion more akin to clean architecture. Note, the first screenshot below is of Page 2’s build() function. You can see its counter resides in the controller object itself. In fact, the business logic involved is also found in the controller, Controller. You can see all this in the second screenshot below. Hence, when incrementing Page 2’s counter, the function onPressed() found in the controller is called. A very clean separation of responsibilities here.

page_02.dart

controller.dart

The second screenshot above also reveals there’s more going on in the controller class. It demonstrates the level of abstraction you can achieve when using a State Object Controller. The count property is, in fact, a getter referencing a property in still another class called, Model. This additional class is only concerned with the data involved in the app. Further, the controller’s onPressed() function eventually leads us to the incrementCounter() function also found in the Model class.

See how there’s a clear separation from the interface, from the event handling, from the data, in this simple app? All ‘keeping it Flutter’ in the process. The State class, Page2State, has no idea there’s a Model class involved — and that’s a good thing. This allows for scalability and better maintainability. It allows for modular and parallel development.

The three screenshots below represent all that’s available to you when using these State Object Controllers as well as the StateX objects. Properties and functions I’ve found that make for a robust and adaptive Flutter app. Not only does the controller provide you with these useful functions and features, but the StateX object can also retrieve particular controller objects by type if necessary. Thus, giving you access to their own State objects in turn and so on. Imagine the possibilities when using this structured and coherent architecture offered by the state_extended package.

page_02.dart

page_02.dart

page_02.dart

A Single Approach

Let’s now move on to Page 3. It too has the means to increment the counters from the previous two pages, and we’ll see that in a moment. First, let me just mention the additional buttons found on page 3 have to do with that annoying black screen that keeps popping up and another important feature used throughout Flutter’s own framework. Both have also been implemented in the state_extended package. It’s all really cool, and we’ll get to that soon, but back to Page 3.

The screenshot below shows you how it's done when it comes to incrementing those other two counters. Again, it involves the class, Controller. Note, however, the unique implementation involved. We’re simply calling that class’ constructor with the appropriate public API appended on the end, and then Bob’s Your Uncle! What the heck is that all about?!

page_03.dart

It’s pretty straightforward. There’s no need for an instance of that controller class just hanging around waiting for an event handler to be called — instead, only call that constructor when a button is pressed. And since the Singleton pattern is being upheld with the Controller class using a factory constructor, there’s little overhead in the approach. Certainly not a steadfast rule, but I tend to have all my controllers instantiate with a factory constructor. Doing so agrees with its general role — only one instance of a particular State Object Controller is ever really necessary for my Flutter apps. A clean, consistent, and manageable way to work with SOC’s. One that adheres to good programming practices.

The Root Of It

Lastly, note the line, rootState?.setState((){});, in the screenshot above. We’re just working with Flutter here when it comes to resetting the counter on Page 1. We know a State object remains in memory retaining state. Its StatefulWidget counterpart, however, may be called again and again and maybe destroyed and re-created again and again while its State object stays in memory. However, call the StatefulWidget with a ‘different key’, and its State object is also re-created. In this case, starting again with a counter set to zero! It’s just a fundamental fact of Flutter being demonstrated here.

The screenshot below is of the ‘first State class’ for the app. You know by now that this state_extended package provides the property, rootState, to reference this particular State object. Now, to reset the counter on Page 1, you simply call that State object’s setState() command. As you know, doing so will cause its build() function to run again, and since the StatefulWidget, Page1, always uses a unique key (Page1(key: UniqueKey())) it’ll re-create its State object as well and then Bob’s your you-know-what! It’s as easy as that.

my_app.dart

Wait-a-minute! What’s this buildChild() function in the screenshot above?! What happened to the usual build() function?? That’s that other cool feature I mentioned earlier, and we’ll get to that soon enough. Just know for now that the ‘root’ State object’s build() function is calling that buildChild() function and therefore re-creating Page 1’s own State object with a counter set to zero.

Wait For The Future

Here and now, let’s address that annoying black screen that keeps popping up in the example app with that circular spinner in the center. That was done on purpose to demonstrate what I felt was an essential feature missing in Flutter’s original State class: The means to deal with asynchronous operations in a State object before proceeding to render the interface. Time and time again, I see other developers performing such tasks even before the runApp() function is called?! Not a very intuitive or efficient approach in my opinion. Keep it clean. Keep it simple. Keep it Flutter.

It’s for demonstration purposes, so there’s no real asynchronous operation going on there. A duration of 10 seconds is counting down when that black screen appears. It’s a Future.delay() function called in a function named, initAsync(), in one of the State Object Controllers. See below. As a result, the app waits for that duration before proceeding.

app_controller.dart

However, in a real production app, this could just as easily be a database opening up, web services being called, servers being logged into, etc. Such operations can now easily be performed in a State Object Controller, and its associated State object will quietly wait until completion with something spinner away on the screen. Nice!

All State Object Controllers have the initAsync() function to run any asynchronous operations before their corresponding StateX objects can proceed and call their build() functions. I use this function all the time and can’t live without it. By the way, as you can see in the screenshot above, you get your State Object Controllers by extending the class, StateXController.

Below are three other gif files. The first one depicts one StateX object waiting to continue while the second depicts twelve StateX objects waiting to continue. Each StateX object has its own individual asynchronous operation going on, and each uses Flutter’s own FutureBuilder widget to wait for completion. See what I did there? I’m keeping it Flutter.

The one above on the far right shows the whole process completed. It’s the startup process for the third example app that accompanies the state_extended package. Since this app is running on an Android emulator, those spinners are from the CircularProgressIndicator widget. However, if it were running on an iOS phone, the CupertinoActivityIndicator widget would be used to produce the iOS-style activity indicators instead. Flutter is a cross-platform SDK after all.

It’s Built-in

So, how do you use the built-in FutureBuilder in the StateX class? Simple. Instead of overriding the build() function, override the buildWidget() function instead. The function name is not very imaginative, but it had to be a different function. You see, in the StateX class, it’s the build() function that introduces the FutureBuilder widget (see the second screenshot below). It eventually calls the buildWidget() function in the internal function, _futureBuilder.

state_extended.dart

state_extended.dart

Update In fact, I’ve changed the function name in a more recent release from buildWidget() to buildF(). It was just too generic a name. I’ll leave it to other developers to use that name for their own projects. The name, buildF(), is more unique and more concise — more descriptive of its task (i.e. this build function involves a Future object).

You can also see in the second screenshot above how the new initAsync() function is involved in the process. Any asynchronous operations found in the initAsync() function are performed and, if it returns a boolean true, will allow the StateX object to continue without an error. Easy Peasy. Of course, if you’re not interested in this feature, you can simply override the build() function and proceed as usual. Gotta love options.

Indeed, in the second screenshot above, a StateX object’s initAsync() function is being called, but in most cases, it is only so to call all of its StateXControllers’ own initAsync() functions. Since most asynchronous operations have no direct relation to an app’s interface, you’ll likely have your asynchronous stuff running in a State Object Controller with the rest of the app’s business logic. See how that works?

Not So Synchronous

Lastly, if an error does occur during the asynchronous operations, you have the means to close any low-level files, for example, and recover from the error as much as you can before StatefulWidget retreats in error. This is in the form of another function called onAsyncError. See below. Note, if you wish the StatefulWidget to proceed regardless, you can always return true from the function. It’s your app after all.

state_extended.dart

The Inherited Child

Now, what was that buildChild() function all about? We’re back at the root State object again in the left screenshot below, and you can see it extends the class, AppStateX. Like Flutter’s own State class, it’s an abstract class, but instead of you implementing the build() function, you’re required to implement the buildChild() function instead. Sure enough, the right screenshot below of the AppStateX class itself reveals this to be true. The AppStateX class extends the InheritedStateX class which extends the StateX class — all found in the state_extended package.

my_app.dart

state_extended.dart

Update buildChild() was also 'too common’ a word for this purpose and has since been changed. A framework is not to be a hindrance and not even take up function names best allocated to developers for their own apps. The function name, buildChild(), has been changed to buildIn() in more recent versions of StateX. It hints at a greater association with the package’s built-in InheritedWidget*.

Let’s further examine the third example app introduced earlier and note, in the screenshot below.
The class, _InheritBirdState, also extends the InheritedStateX class. So, what is all this leading to? Well, take a look at the video below. If you’ve deduced this example app is displaying images from some public web service, you’d be right. It displays a random batch of photos from four such services depicting birds, cats, dogs, and foxes. For example, the InheritedStateX subclass displayed below is only concerned with the REST API supplying bird photos.

inherit_bird.dart

The screenshot above further introduces an InheritedWidget called, _BirdInherited, which is explicitly instantiated in a callback method passed to the InheritedStateX’s constructor. That InheritedWidget is thus inserted into the Widget tree by this class.

By the way, there are those circular spinners again in the video as well, so you know that the built-in FutureBuilder widgets are being used. That makes sense. With such network operations, it takes time to complete the connection and download the initial images and so the widgets are waiting.

Build For Performance

Lastly, in the video above, you can see that pressing the ‘new’ TextButtons, in turn, will update the appropriate images. Three widgets are being updated with every tap. Only three of the twelve widgets are ever updated in each instance — the rest of the screen is left alone resulting in better performance. Huge. Note, there aren’t three separate setState() functions being called with every tap either. There’s only one. Keeping it Flutter — that one setState() call will call an InheritedWidget again somewhere, and so its ‘dependent’ widgets are then spontaneously rebuilt. A powerful characteristic of the InheritedWidget.

I should emphasize this. Most of the screen is left untouched with every tap. Again, huge. You’re app’s likely running on a phone and not a Cray supercomputer. And so, if you can get away with ‘repainting’ the screen as little as possible — that’s a good thing. Look closely at the three videos above. Imagine the waste if the whole screen, TextButtons and all, had to be rebuilt. It’s a bad example, frankly, all the web services would then return new images, and the app wouldn’t behave as intended. The package’s second app would be a better example, but that’s later. For now, in this case, only three separate areas of the screen are affected by every tap. Very efficient. Huge.

Each ‘set of animal pictures’ has its own InheritedWidget, and so all three State objects that make up a set are ‘linked’ to their respective InheritedWidget using the command highlighted below. Easy peasy.

image_api.dart

When it comes to retrieving a new set of pictures of a particular animal, the notifyClients() function is then called which, in turn, calls that one setState() function. See below. Now, I won’t get into the details here at this point. This article is long enough as it is. Besides, you can examine the app and see how it’s done.

state_extended.dart

Depend On Inheritance

Again, the most important thing you should remember about InheritedWidgets is this: When a particular InheritedWidget is called again and allowed to notify its ‘dependent’ widgets, all those widgets’ build() functions are called again. In my view, that’s the most important reason to use InheritedWidgets. They’re found throughout the Flutter framework itself utilizing that very ability.

To get just a little deeper into the details, the widgets that display the animal pictures extend the State class, ImageAPIStateX. We have the ‘bird version’ in the first screenshot below. The second screenshot displays the buildWidget() function used by the ImageAPIStateX class, and again the function, dependOnInheritedWidget, called by that State object’s controller connecting that widget to the appropriate InheritedWidget. The third and last screenshot on the right displays the notifyClients() function used to retrieve and update just three of those twelve widgets. Very nice.

random_bird.dart

image_api.dart

inherit_controller.dart

A Light Touch

Let’s finally end this article with the second example app that accompanies this package. This app is presented in the video below and is a variation of the counter app. With every fifth tap on the plus-sign button, a new greeting appears in red. Know the App’s ‘first’ State object is involved here, and, as you know, it has its own InheritedWiget. In this app, there’s no setState() function ever called with every tap of the plus-sign button! Instead, when the InheritedWidget is called again with the notifyClients() function, its dependent widgets are rebuilt.

Keep it Flutter!

home_page.dart

The two red circles on the above right graphic highlight the only two widgets on the screen that is ever ‘repainted’ with every tap of that button! In this simple app example, the count number itself is encased in its own StatefulWidget — its State class is displayed in the screenshot above. You can see here the function, dependOnInheritedWidget, is used again. This time linking that widget to the App’s main InheritedWidget associated with the root StateX class, AppStateX. Doing this, only the count and the greetings have to be repainted on the screen with every tap leaving alone the FloatingActionButton widget and the ‘You have pushed the button this many times:’ Text widget. Again that’s so huge.

Sure it’s a very very simple app and granted it’s not by much, but there would still be a greater expense if you had to refresh the whole screen. You’ll likely encounter much more busy interfaces in projects to come — so keep InheritedWidgets in mind in any case.

A Wall Street share price board

Let’s stop here. I’ll leave you to run those example apps yourself — and step through the code. So my approach was to extend the capabilities of what Flutter already offered in its State class. If I don’t say so myself, I feel it’s a well-structured and coherent approach. I do see the popularity of those other packages — people go with what they know. However, we’re Flutter developers now. Keep it Flutter.

Cheers.

Greg Perry

TL;DR

It’s In The Testing

For those of you who want to go deeper and appreciate what StateX has to offer, examining the very tests run during GitHub’s Continuous Integration would be a good start. Of course, you’ve ready access to them in the package’s repository. However, some are listed individually below:

test_statex.dart
test_controller.dart
test_error_handling.dart

Top comments (0)