More like State access, no? Conceptual discussion.
As you know, there’s a great number of architectural frameworks offered to Flutter developers to supply some form of State Management to their apps. I even threw in an offering that emulates a time-tested design pattern. However, I won’t promote it here. That would be uncouth.
Back in March of 2018, all I wanted was to get rid of two warning messages that I was always encountering when first learning Flutter. These messages were telling me I was doing something wrong and not optimizing my code to take full advantage of Flutter's unique characteristics. You know the messages I’m talking about, don’t you?
‘@immutable’ and ‘setState’
Those two messages were presented to me the first day I started playing around with Flutter. Of course, I didn’t understand why at the time, but StatelessWidgets and StatefulWidgets are to contain unchanging (immutable) instance fields/properties, and you’re not to call a State object’s setState() function directly but through a subclass instead.
Note, you now have an array of frameworks and architectures available to you to overcome these warning messages. However, as I know now, it all comes down to a separate class for the immutable properties of your app and utilizing a subclass of the State class. To be more specific, reliable access to a particular State object so one can call its setState() function and ‘refresh’ that portion of the screen and reflect changes to the app; changes to state.
Separating the interface from everything else that makes for better progress in development as well as better scalability and maintenance down the road.
Now, two years in, I went the long way around and created a whole architectural framework that, if I don’t say so myself, seamlessly works with Flutter while emulating a well-founded design pattern. It’s my chosen approach to software development in Flutter frankly. However, again, not promoting it here.
In fact, today, I’ll give you a stretch of code that I feel will be all you need for most of your Flutter apps. One Dart library file with one class and a couple of mixins will be all you need to serve as your app’s ‘State Management.’ I mean, the Flutter team has already provided us with a ‘State Management’ mechanism involving State objects! Despite that, we’ve been slapping on top of Flutter every manner of architectures in an effort, I suspect, to supply developer’s something they’ve worked with before (Time is money after all.). Redux comes to mind. Redux!?
I Like Screenshots. Click 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 these screenshots to see the code in a gist or in Github. Further, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program mostly on our computers — not on our phones. Not yet anyway.
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, Facebook, etc. They may come out as static pictures or simply blank placeholder boxes. Please, be aware of this and maybe read this article on medium.com
As many of my readers know, I usually use well-established examples to demonstrate the topic at hand in my articles. This time, we’re back to the time-honored ‘startup app’ (the counter app) that’s presented to you when you create a new Flutter project. It’s a classic example. However, this time, we’re going to ramp it up a bit to fully demonstrate the little Dart library file I’m presenting, today.
In fact, let’s not keep with the suspense. Let’s display that Dart file right here and now, and get it over with. Most of it is listed below. Mind you, there’s really no need to examine it for too long. It’ll be demonstrated soon enough with the example. I would suggest just scroll down past it (noting how little of it there is) and realize this is all the State Management you’ll need for most of your Flutter apps.
Divide And Code
Now, as I’ve stated in past articles before, for a good computer application, what you need to do is divide ‘the interface’ from everything else that makes up your app. Doing this alone makes for better progress during development and better scalability and maintenance down the road.
As I’ve also stated in past articles, there’s one common trait found in all the frameworks offered these days — the access to particular State objects during the execution of your app. More specifically, access to their ever-important setState() function.
Below, listed with the first half of the example app we’re using today, is a gif file demonstrating the functionality of that example app. Note, there’s not one, not two, but three State objects (Flutter pages) that make up this example app. In fact, there are four State objects in all — you’ll see that soon enough. Regardless, there are four ‘states’ being retained and managed. It’s an attempt to convey a typical Flutter app in the guise of a very simple example app, and so it’s a counter app with three counters. Their pages come one after the other. While running, you can go up and down the routing stack. The app is demonstrating the ‘state management’ involved.
The Classic State
In the gif above, you see we’re going up through the three pages incrementing their individual counters along the way. There’s a number of buttons offered so to navigate up and down the routing stack. As anticipated, returning back down the pages then up again, the counter on the third page is reset to zero. This makes sense. The app had retreated back down the routing stack, and the State object retaining the counter (the state) on the third page was terminated (its dispose() function called). Perfectly normal. However, notice the second page is keeping its counter value?? How is it doing that?!
Further note, on the second and third pages, you can increment the counters on the previous page! On the third page, for example, there are two buttons to increment both the second page and the home page. Well, that’s neat. Now how is that done? Finally, pressing that ‘Home Page New Key’ found all the way up on page three results in the counter on the first page (the Home page) to be reset to zero! Now, what’s going on there?
Granted, this is such a rudimentary example, but it does demonstrate some fundamental aspects involved in the State Management of a typical Flutter app. I’ll put to you that all the frameworks offered, today, use the same basic mechanism to provide such capabilities (some all be it in a cumbersome and rather round-about way in my opinion).
Use the setState() function to a particular State object.
Let’s examine the first State object responsible for displaying a screen to the user. In this case, it’s the ‘Home page’ greeting the user with the first counter. Below is a screenshot of that State with little red arrows highlighting points of interest. What do you see?
First and foremost, you can see the example app is using a subclass of the State class called, SetState. Yes, this class is the main point of interest in this article. Certainly using a subclass of the State class now relieves us of that one annoying warning message about using the setState() function.
You’ll find all the frameworks out there have a ‘subclass’ of the State class in one form or another. All of which supplying a means to use the setState() function to a particular State object — whether the developer knows exactly which State object that is or not is another matter.
What else do you see? Well, there’s ‘the separation of work’ that I personally live by when developing code. More specifically, there’s always a separate and dedicated class (_HomePageBloc in this case) that’s responsible for the actual ‘business logic’ involved in the app while the build() function in some widget somewhere is responsible for the interface.
Further, there’s the degree of abstraction that I always implement as the API between these separate areas of responsibility. As an example, the actual ‘counter’ here in this app is concealed by the class property, data. I follow the consistent practice of naming instance fields after the parameter used by the receiving Widget. As it happens, the Text widget’s first parameter is named data. All this a consistent approach — dare I say, a design pattern.
Now, in keeping with the topic at hand, let’s examine two points of interest in particular. What I’ll be presenting from now on will be concepts subtle in nature but very important characteristics to uphold when writing good code.
The three Bloc classes (no direct relation to the BloC design pattern) you’ll find in this example are indeed the ‘Business Logic Components’ for the app. Each has its own little bit of responsibility (their own little bit of ‘state to manage’). They’re also the app’s event handlers — each response to particular events that may be triggered by the user or by the system (the phone) itself.
The screenshot below presents the first Bloc somewhat named after the State object it’s to work with. It even explicitly takes in the type of that State object it works with. At a glance, you can see this Bloc class is for the home screen.
Now, why is the Bloc simply taking in ‘the type’ of the State object? Why not take in the very State object itself? Seems to be another rouned-about way of doing things, no? Lastly, note the Bloc class is found in the same Dart library file with its leading underscore. For demonstration purposes, this was the case. However, in practice, I’d suggest the ‘business logic’ deservers its own Dart file.
There’s another question here. Why was an altogether separate function called onPressed() created in the State object? See below.
I mean, in some design patterns offered today, it’s customary to simply find the corresponding ‘onPressed’ VoidCallback function in the Bloc class and use that instead as depicted in the screenshot below.
However, when you do see such functions in a State object, know that this object is now providing an API. Therefore, it must be necessary for external players to also trigger an event in this object. You’ll soon realize who these players are in this particular example app. Hint: Buttons.
Let’s backtrack a bit and take a deep dive into the instantiation of this first Bloc class, _HomePageBloc. In the screenshot below, you can see we’ve jumped ahead a bit to examine this Bloc class. In fact, we’ll examine all three of them. You can see all three Blocs in this app take advantage of inheritance extending from the common parent class, _CounterBloc. After all, they all pretty much do the same thing and so that function is found in one parent class — working with an integer called, counter.
Again, it would have been better to have these Blocs in their own separate Dart library files. For example, if they were in their own file, you’d then be free to extend a different parent class altogether in the future — displaying string ‘word pairs’ let’s say while unaffecting the rest of the app. The parent class, _CounterBloc, should be in its own Dart library file as well preferably with a more generic class name. A degree of abstraction always makes for easier maintenance of an app in production.
Regardless, note the first Bloc class, _HomePageBloc, doesn’t know the type of State object beforehand, and so that type is passed in as a generic type. While in the second Bloc class, _SecondPageBloc, the State type is known and is explicitly specified. Which approach to use, of course, depends on the circumstance. At least, you’re free to use either. You have that option.
Further note, the second class utilizes a factory constructor, and that’s how it retains its count even when you retreat back down the routing stack! In every Flutter app you write, returning to a previous page will remove a Page from the stack, and if it’s represented by a StatefulWidget, that means the StatefulWidget’s State object will be disposed of. Every time. Unless you do something about it.
You’ll note, when it comes to the second page, the ‘State Management’ has been allocated to a separate class altogether and not left to the State object. Following the Singleton design pattern, the _SecondPageBloc class remains in resident memory for the life of the app. Thus keep its counter (its state) and is assigning a brand new State object to itself (the second arrow) whenever a user comes back to that page.
Now, let’s look at the third and final Bloc class, _ThirdPageBloc. Note that the instance field, state, is successfully overridden with a getter. THIS IS HUGE! Do you know why? It’s huge you can successfully override a mutable instance field with an immutable getter! Since a getter is essentially ‘instantiated’ only when it’s first used, you can provide the ‘type of state’, but you’re not obligated to instantiate a reference to that State right then and there! You can wait. Possibly in some situations, the State is not to be instantiated at that point — It may not be available for some reason.
We might as well take a look at that parent class to the Bloc’s now. Again, it’s an event handler. Such a class is required to respond to events. In this case, the most profound event is when a user taps on the plus sign to increment the counter. Thus, the most important capability of this class is to then notify the appropriate State object when it’s completed responding to that event.
As you know, it does have access to the ever-important setState() function for a particular State object. It takes advantage of that access and even defines its own setState() function — for any other modules to then call to notify the app and reflect a change. Finally, it offers a corresponding dispose() function to be called in its State object’s own dispose() function when the State object itself indeed terminates during the course of the app’s lifecycle. It’s all nice and compact. However, we could do better.
Note, such abilities, on the whole, should be present in any and all modules that are to work with a SetState object in this fashion. Such abilities should be readily available to any class you may define to contain the ‘business logic’ of an app. Such a circumstance would therefore be a good candidate for a mixin, no? A screenshot of that mixin is below.
That parent class, _CounterBloc, has now been changed — focusing truly now on the one lone functional responsibility assigned to it in this particular app. When it increments its counter, it then notifies the rest of the app with the setState() function. It now takes in the mixin using the keyword, with, resulting in code generally being more modular. Further, as you see below, there’s a higher cohesion in the resulting class.
Navigating The State
Looking at the third page in the routing stack, we see it presents to the user five buttons. The first three buttons literally affect ‘the state’ in three separate regions in the app. The first button calls upon a provided Bloc object to respond to the event of incrementing that page’s counter. The next three buttons involve the State objects from ‘previously visited’ areas of the app. Each is responsible for retaining their own state. Note, the names of the VoidCallback functions of the State objects, homeState, and secondState: onPressed. Should the fourth and last State object also have such a function?
Pressed For Consistency
The State objects up until the last page was mirroring the API of their corresponding Bloc objects. There are ‘public’ onPressed() functions in the State objects that call their Bloc object’s own onPressed() function.
The graphic below conveys the lines of communication of what’s occurring here. The State object (View) and its associated Bloc (Controller/Model) are ‘talking’ back and forth. Further, with their ‘public’ functions, these State objects are now accessible, as it were, by external modules.
For better or worse, I’ve decided to include a public onPressed() function in the third-page state object as well — merely a campaign for consistency.
Attain Your State
Our last look at the third-page State object highlights the four approaches taken to supply the event handling and business logic, as it were, onto this last page. Most of them involve a static function from, in fact, the StateSet mixin.
Note, all but the last State object instantiated is a specific type unique to this app. In other words, this class (this module) has to explicitly know the names of the other classes involved in this app to function. The last instance field, however, is assigned the foundational type, SetState.
Polymorphism is in play here. The first ‘SetState’ object (retrieved by SetState.root)can be anything — we don’t know what from this vantage point. However, we do know it is a ‘SetState’ object. More so, we know it’s a State object, and so we know we can call its setState() function from this class — from this vantage point. What it does when we call this object’s setState() function, we can only guess. We’ll look into what exactly happens next.
A New Home
Remember, pressing that ‘Home Page New Key’ button resets the counter to zero on the Home page. Actually, what it does is re-create the State object itself displaying that page! Now does that sound right to you? After all, State objects are to stay in memory retaining their state, right? I mean, that’s Flutter's whole State Management approach. This is ‘the first’ and founding State object for this app. It doesn’t terminate until the app does, no? However, if you assign a new key to a State object’s corresponding StatefulWidget, that State object is disposed of and a new one created. Let’s walk through it and show you how this happens.
Let’s look at the two screenshots below. The screenshot on the left-hand side below reveals when you press the ‘Home Page New Key’ button, the ‘app state’ State object calls its setState() function. That setState() function is displayed on the right-hand side includes assigning a new value to the instance field, _homeKey. That field, as you see in the build() function above, is passed as the key to the StatefulWidget, MyHomePage. Calling setState() will run the build() function once again, however this time, with a new key.
Calling the setState() function from the State object, appState — the fourth State object I spoke of, with its the StatefulWidget, _MyApp, being passed to the runApp() function, will cause its build() function to fire again. This means the named parameter, home, receives a StatefulWidget with a brand new key. In turn, this means that the StatefulWidget’s createState() function is fired again and a new State object is created. One that calls its initState() and build() functions again.
Set Your State
Let’s finally take a look at the class, SetState, replacing the traditional State objects in the app. Being abstract, of course, you have to implement its build() function, but you still have access to the usual and useful properties found in a State object: mounted, context, and widget. You can see in the screenshot below, this State object adds itself to the collection of State objects used by your app. It removes itself from that collection when it itself terminates. Finally, as you see below, it has its ever-important static function, of. You’re familiar with such functions in Flutter: Theme.of(), and Scaffold.of().
Magic In The Mix
You’ll note the mixin, StateSet, utilized in the abstract class above. It is this mixin that stores the collection State objects. Further, looking at the mixin below, you now have a subclass of the State class allowing you to call the setState() function without complaint.
I liked it so much, I published it as a package, set_state. Wish I wrote this two years ago. I’m certainly going to use this for my smaller projects. It’s all I need to make a responsive and functional app 2 to 3 pages deep. It does in a more concise approach, what all the other frameworks, including my own, offers — a standard approach to accessing the appropriate State object whenever necessary. Try it. I’ll make you a believer yet.