Switching between Material and Cupertino.
Let’s demonstrate how Flutter allows you to ‘rebuild’ only certain branches of its widget tree when you wish. StatefulWidgets built upon other StatefulWidgets is a very common way to selectively choose ‘how much’ of your Flutter app’s interface is essentially rebuilt from scratch.
As in many of my articles, I’ll work with a well-known Flutter example to demonstrate the topic. Today, we’ll go to the old Write your first Flutter app example — with some modification of course. Unlike the original, there are two State objects in this example. You’ll see what each is responsible for shortly. As you know, a State object retains its state; retains its data. However, with every subsequent re-build (calling its build() function), you have the opportunity to change something — more accurately you have the opportunity to reflect a change in the interface.
Look closely at the screenshot below. It’s a State class. In fact, it’s a subclass of another subclass of a State class called, _MyAppState. It extends the class, StateBloc. Its build() function provides an opportunity to change something. In this case, that something is the very interface design of the app itself to either Material or Cupertino.
You can see, in the screenshot above, the build() function returns a widget called, _RandomWords, but not before wrapping it in the appropriate ‘design’ widget: CupertinoApp or MaterialApp. You can see there’s a property called, useCupertino, from the class, _AppEventHandler, that clearly determines which one is used with every call to that build() function. Further note, the class, _AppEventHandler, takes in the State object, _MyAppState, when first instantiated. In this article, you’re going to see why.
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
So, in that screenshot above, the build() function returns a widget called, _RandomWords. Looking at the class below, we see it too is a StatefulWidget and, it too works with an instance of that _AppEventHandler class. This time, the property, useMaterial, is used from that class to determine which State object to instantiate and return from the function, createState(). There appears to be a particular approach used here, no? Dare I say — a pattern.
The build() function from both State classes (the Material version and the Cupertino version) is listed below side by side. Of course, the code in each function is different because of the varying style and appearance to be conveyed. However, note the little arrows. Both build() functions also use a reference to that _AppEventHandler class. As it happens, the instance field is named, handler, instead of named, appBloc. Both build() functions also use a second Bloc to supply the very data as well as be responsible for any event handling. Yep, definitely a consistent approach (a pattern) is being used here.
Both build() functions have the same double-arrow icon in their navigation bars, and both utilize the same function in the _AppEventHandler class called, leading. By the way, a screenshot below of the ‘Material’ interface design highlights that icon — you can’t miss it.
Let’s take a quick peek at that function called, leading, in the class, _AppEventHandler. You can see below it, in turn, calls a function with a more descriptive name. Further, you can see the switching of the interface design would seem to come down to the toggling of a boolean instance field called, useMaterial. You’ll discover, in fact, that that is a getter & a setter in that class.
Set The Interface
Note, in the screenshot below, the setter involves a couple of static instance fields. Being static they retain their values for the lifetime of the class, _AppEventHandler — in any number of instances of that class. Assigning a boolean value to the setter sets the appropriate values to those static fields. They effectively serve as conditional flags and can be readily accessed with every new build. Are you beginning to see how things are working here?
Well, how does the next build come about, and more importantly, what gets rebuilt? Let’s look at the ‘switch interface’ function again, and note the refresh() function coming from that object, appState. See what’s happening?
That instance field, appState, references the first State object, _MyAppState. _MyAppState is a subclass of the StateBloc class which has the refresh() function. The StateBloc class extends Flutter’s State class. There’s a screenshot of that refresh() function below. It’s calling the State class’ setState() function with an empty lambda in it. Guess what that does.
It results in the _MyAppState object’s build() function eventually being called again. And, as you know, there’s some logic in that build() function used to determine which interface design to use. See how it all works now?
CClicking on that double-arrow icon, a particular State object calls its setState() function marking the element as ‘dirty’ (to be rebuilt) and its StatefulWidget is then added to the global list of widgets to be rebuilt in the next scheduled frame. As it happens, the State object itself is also re-created resulting in that whole branch of the widget tree being rebuilt, and in this simple app, that means the whole app is rebuilt. Easy, peasy.
And so, looking at the screenshot below, we see a ‘design’ widget is being instantiated again and again with every build. However, note the little red arrow. The StatefulWidget, _RandomWords, is being instantiated again. That’s alright — StatefulWidgets get re-instantiated all the time in Flutter. However, what’s the deal with assigning a new unique key every time?
New Key, New State
With a new unique key assigned in the StatefulWidget, Flutter will now call that StatefulWidget’s createState() function re-creating its State object. If a new unique key wasn’t assigned to the StatefulWidget, by design, the State object should remain in memory — retaining its state. Granted, there are instances where the State object is considered ‘dirty’ as well, but this approach assures it will always be rebuilt. Most times, it is not to be rebuilt. After all, in Flutter, though a StatefulWidget is destroyed and rebuilt many times, its State object is to remain — to retain its state as part of Flutter’s State Management.
However, a new unique key assures a State object’s rebuild. Now, look below and see what that means. It means the home screen will then display the appropriate ‘look and feel’ depending on its designated interface design. The conditional expression,? :, makes sure of that.
By the way, below is a screenshot of simply returning the State class, _RandomWordsAndroid, every time — even though the designated interface design is Cupertino. You can see they don’t mix and match, and so, the ‘App Event Handler’ is utilized throughout the app to ‘retain a consistent state’ — a consistent look and feel in this case.
Also, notice how the _AppEventHandler class is all self-contained. When first instantiated, it determines which platform the app is running in. Further, the first State object it registers is then designated the ‘app state.’ It’s the app state that’s rebuilt again and again when the double-arrow icon is tapped on. With its static fields, the _AppEventHandler class retains its current state — that state being ‘what could and should be the current interface design with every rebuild.’ Lastly, notice in the screenshot below, the class uses a factory constructor (singleton pattern) ensuring only one instance of this class for the lifetime of the app — further retaining a consistent state.
Also note, any function calls in this class involve instance fields inside the very same class. There’s high cohesion here. There’s some good parameter validation as well. For example, if there’s no ‘app state’ specified (no StateBloc object supplied to the constructor), there’s no error — simply nothing happens when the user taps on those double arrows for example. That’s it. A developer will notice that problem. Users won’t be calling your company complaining there’s an error — “it’s just not working??”, they’ll say.
Note the low coupling. The rest of the app, for example, doesn’t know exactly how the _AppEventHandler class determines the current platform used. It doesn’t need to. That means if we change ‘the means’ used to determine the current platform, the rest of the app will be none the wiser. For example, the class, Platform in dart:io, doesn’t currently allow this simple app to work on the Web. However, that can easily be solved by switching it out with the package, universal_platform, allowing the app to also run on the Web.
Divide And Conquer Code
The code would have to change a bit, however. I mean, first and foremost, I feel it’s best to separate the ‘logic’ from the ‘interface’ of an app into different Dart files altogether. However, being such a simple example app and for brevity, that wasn’t done here. It’s my usual practice, however, to include only the import statements that are relevant to the code found in a particular Dart file. It’s a standard approach. A pattern? A pattern!
If the class, AppEventHandler (no leading underscore), was in a separate Dart file; a separate library file, no other part of the app would know it’s now importing the package, universal_platform, for example. The app could now even run on the Web. And if on the Web, let’s say it’s to use the Material interface design. ok? Again, the logic is all encapsulated in one place. Any such changes are isolated to this one class. Modular approach. Makes for better maintainability.
Reuse The Code
Looking closely at the code in the class, _AppEventHandler, you can see there are other patterns in play. Inheritance, of course, allows you to take advantage of reusable code (the code in the parent class) and yet adjust and enhance the same operation to the particular needs of the child class.
As an example, the function, addState(), is called in the private constructor called, in turn, by the factory constructor when the class is instantiated for the very first time. It’s a function originally found in the parent class, Bloc, and serves to designate the specified State object as the Bloc’s ‘current’ State object. — it now is to work with that State object. You see, a single Bloc should be able to work with more than one State object for the duration of the Bloc’s lifecycle — makes for a more useful Business Logic Component. No?
You see the first time the addState() function is called, the ‘app state’ is forever assigned. The way this is done is subtle but effective. The assign-if-null operator, ??=, records only the first occurrence when a state object is passed to the function, addState(). Any subsequent calls to the function, addState(), allows the Bloc to ‘talk to’ a different State object (the most recent State object) by design. However, when it comes to calling the switchUI() function (through the leading() function), only the ‘app state’ object is refreshed. See how that works?
If later we no longer like that particular approach, we can make a change sometime in the future. The change will likely only occur in this class— only affecting this class. There’s room for scaling since the code is modular.
The State of Abstraction
Did you notice the inheritance utilized when it came to those two State objects? Both the ‘Android’ state class and the ‘iOS’ state class extends from the class, _RandomWordsState. Why did I choose to do that? Repeated code.
It’s a mantra for we developers, right? ‘No repeated code.’ Maintaining literally the same bit of code in more than one location in an app is a nightmare. It’s a big no-no, and inheritance would be a go-to solution. However, Inheritance has been under scrutiny in recent years. Rightly so, you should be frugal when it comes to implementing inheritance every time and everywhere since Composition has proven to be more adaptive in practice. Further still, inheriting from an abstract class has proven to be troublesome at times when it came to making changes down the road, and so using an interface instead of an abstract class has become more popular.
However, in this particular case, I felt inheriting an abstract class was appropriate. You’ve got two separate classes (both mutually exclusive) that need the very same abilities as it were. They both are strongly coupled to the class, _RandomWords (Of course they are. They’re the State object to that StatefulWidget). Finally, they both had to implement the build() function anyway since Flutter’s State class is an abstract class as well.
Separation of State
Below is a screenshot of the abstract class, _RandomWordsState. Both subclasses of this class use the ‘business logic components’ highlighted below. Both require the registering of one Bloc in the initState() function. Finally, both are left with just the one distinction between them: a completely different set of widgets returned from their corresponding build() function.
Having these two State classes with only the build() function listed makes for further cohesion. For example, if a developer was put to task to work on the Cupertino interface in the class, _RandomWordsiOS, they need not have access to the class, _RandomWordsState, at all. They don’t have to know what the parent class does. Their job is just the iOS interface conceived in the build() function— and all they really need to know is the API (the names and the syntax) to use the Bloc’s in the class, _RandomWordsState. See the separation there? Makes for a better development process and better maintainability.
So there you have it. Targeting particular branches of the widget tree by calling the setState() function of specific State objects. Their build() functions will then be called again. Assign a new key to a StatefulWidget found in that build() function for example, and you‘re assured it’ll re-create its own State object as well. Finally, encapsulating the functionality and state of a particular operation inside a class makes for more efficient development and maintenance. Easy peasy.