Out of Depth with Flutter
Flutter is a new framework for building high-quality native apps for iOS and Android in record time. In my experience using Flutter (as a member of the Flutter team), development speed is achieved primarily through the following:
- Stateful hot reload. The Flutter development experience is powered by Dart compiler/VM technology, allowing you to load code changes into a running app while retaining the app state (including the place you’ve navigated to). Hit save and you’ll see the effect of the change on device in less than a second.
- Reactive programming. Flutter follows other modern frameworks in its approach to defining and updating the user interface: both happen based on a single description of how the interface depends on current state.
- Composition. In Flutter, everything is a widget, and you achieve any desired outcome by freely composing focused widgets, Lego brick style.
- UI as code. Flutter does not come with a separate layout markup language. Each widget is written in Dart only, in one place, eliminating syntax switching and file switching overhead.
Interestingly, the last three bullets above conspire to form a challenge to development speed: losing your way and your view logic inside deeply nested widget trees.
Below I’ll discuss why this issue arises and what you can do about it. Along the way I’ll try to shed some light on how Flutter works.
Reactive programming
Flutter’s reactive programming model invites you to use declarative programming to define your user interface, as a function of current state:
@override
Widget build(BuildContext context) {
return // some widget based on current state
}
Widgets are immutable descriptions of user interface. We are asked to return a single widget defined by a single expression. There is no sequence of mutator commands to configure or update a mutable view. Instead, we just call some widget constructor.
Composition
Widgets are typically simple, each doing one thing well: Text
, Icon
, Padding
, Center
, Column
, Row
, … To achieve any non-trivial outcome, many widgets must be composed. So our single expression easily becomes a deeply nested tree of widget constructor calls:
Widgets have properties other than child/children, but you get the idea.
UI as code
Writing and editing deeply nested trees requires a decent editor and a bit of practice to become efficient. Developers seem to tolerate deep nesting better in layout markup (XML, HTML) than in code, but Flutter’s UI-as-code approach does mean deeply nested code. Whatever view logic you might have inside your widget tree — conditionals, transformations, iterations used while reading current state, event handlers used for changing it — also gets deeply nested.
And that, then, is the challenge.
The challenge
The flutter.io layout tutorial provides an illustrative example using — so it seems — a lake explorer app.
Here is a raw widget tree implementing this view:
This is just a static widget tree, implementing no behavior. But embedding your view logic directly into such a tree might not be a pleasant experience.
Challenge accepted.
UI as code, revisited
With Flutter’s UI-as-code approach, the widget tree is, well, just code. So we can employ all of our usual code organizing tools to improve the situation. One of the simplest tools in the box is naming subexpressions. This turns the widget tree inside out, syntactically. Instead of
return A(B(C(D(), E())), F());
we might name every subexpression and get
final Widget d = D();
final Widget e = E();
final Widget c = C(d, e);
final Widget b = B(c);
final Widget f = F();
return A(b, f);
Our lake app can be rewritten as follows:
The indentation levels are now more reasonable, and we can make the subtrees as shallow as we want by introducing more names. Even better, by giving meaningful names to individual subtrees, we communicate the role of each. So we can now talk about the xxxAction
subtrees… and observe that we have a lot of code duplication across those! Another basic code organizing tool — functional abstraction — takes care of that:
We’ll see a more Fluttery alternative to plain functional abstraction in a bit.
Composition, revisited
What next? Well, the build
method is still rather long. Maybe we can extract some meaningful pieces… Pieces? Widgets! Flutter widgets are all about composition and reuse. We have composed a complex widget from simple ones provided by the framework. But finding the result too complex, we can opt to decompose it into less complex, custom widgets. Custom widgets are first-class citizens in the Flutter world, and sharply defined widgets have great reuse potential. Let’s turn the action
function into an Action
widget type and place it in a file of its own:
Now we can reuse the Action
widget anywhere in our app, precisely as if it was defined by the Flutter framework.
But hey, wouldn’t a top-level action
function serve the same need?
In general, no.
- Many widgets are constructed from other widgets; their constructors have
child
andchildren
parameters typedWidget
andList<Widget>
. So theaction
function cannot be passed to any of those. Of course, the result of invokingaction
can. But then you would pass a widget tree pre-built in the current build context, rather than aStatelessWidget
that builds its subtree only if needed and in the context defined by wherever it ends up in the overall tree. Noticed the expressionTheme.of(context).primaryColor
at the start ofAction.build
? It retrieves the primary color from the nearestTheme
widget up the parent chain — which may well be different from the nearestTheme
at the point whereaction
would be invoked. Action
is defined as aStatelessWidget
which is little more than a build function turned into an instance method. But there are other kinds of widget with more elaborate behavior. Clients ofAction
shouldn’t care what kind of widgetAction
is. As an example, if we wanted to endowAction
with an intrinsic animation, we might have to turn it into aStatefulWidget
to manage the animation state. The rest of the app should be unaffected by such a change.
Reactive programming, revisited
State management is the cue to start taking advantage of Flutter’s reactive programming model and make our static view come alive. Let’s define the state of the app. We’ll keep it simple, and assume a Lake
business logic class whose only mutable state is whether the user has starred it:
abstract class Lake {
String get imageAsset;
String get name;
String get locationName;
String get description; int get starCount;
bool get isStarred;
void toggleStarring(); void call();
void route();
void share();
}
We can then construct our widget tree dynamically from a Lake
instance and, as part of that, set up event handlers to call its methods. The beauty of the reactive programming model is that we only have to do this once in the code base. The Flutter framework will rebuild our widget tree whenever the Lake
instance changes — provided we tell the framework about it. That requires making MyApp
a StatefulWidget
which in turn involves delegating widget building to an associated State
object and then calling the State.setState
method whenever we toggle starring on our Lake
.
This works, but is not particularly efficient. The original challenge was a deeply nested widget tree. That tree is still there, if not in our code, then at runtime. Rebuilding all of it just to toggle starring is wasteful. Granted, Dart is implemented to handle short-lived objects pretty efficiently, but even Dart will eat your battery, if you repeatedly rebuild the world — especially where animations are involved. In general, we should confine rebuilding to the subtrees that actually change.
Did you catch the contradiction? The widget tree is an immutable description of the user interface. How can we rebuild part of that without reconstructing it from the root? Well, in truth, the widget tree is not a materialized tree structure with references from parent widget to child widget, root to leaf. In particular, StatelessWidget
and StatefulWidget
don’t have child references. What they do provide are build
methods (in the stateful case, via the associated State
instances). The Flutter framework calls those build
methods, recursively, while generating or updating an actual runtime tree structure, not of widgets, but of Element
instances referring to widgets. The element tree is mutable, and managed by the Flutter framework.
So what actually happens when you call setState
on a State
instance s? The Flutter framework marks the subtree rooted at the element corresponding to s for a rebuild. When the next frame is due, that subtree is updated based on the widget tree returned by the build
method of s, which in turn depends on current app state.
Our final stab at the code extracts a stateful LakeStars
widget to confine rebuilds to a very small subtree. And MyApp
is back to being stateless:
It seems sensible to decouple a generally applicable Stars
widget from the Lake
concept, but I’ll leave that as an exercise for the reader.
Having successfully added view logic to the code, at manageable nesting depth, I think we have arrived at a reasonable response to the deep nesting challenge.
One could imagine several interesting technical solutions to the problem of losing track of your Flutter view logic inside deeply nested widget trees. Some of them might require changes to the Flutter framework, to IDEs and other tooling, or maybe even to Dart language syntax.
But there are powerful things you can do already today, simply by turning the causes of the problem — UI as code, widget composition, and reactive programming — to your advantage. Getting rid of deeply nested syntax is only the beginning of a journey towards readable, maintainable, and efficient mobile app code.
Happy Fluttering!