Out of Depth with Flutter

Mikkel Ravn
Flutter
Published in
7 min readJul 10, 2018

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 and children parameters typed Widget and List<Widget>. So the action function cannot be passed to any of those. Of course, the result of invoking action can. But then you would pass a widget tree pre-built in the current build context, rather than a StatelessWidget that builds its subtree only if needed and in the context defined by wherever it ends up in the overall tree. Noticed the expression Theme.of(context).primaryColor at the start of Action.build? It retrieves the primary color from the nearest Theme widget up the parent chain — which may well be different from the nearest Theme at the point where action would be invoked.
  • Action is defined as a StatelessWidget 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 of Action shouldn’t care what kind of widget Action is. As an example, if we wanted to endow Action with an intrinsic animation, we might have to turn it into a StatefulWidget 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!

--

--

Mikkel Ravn
Flutter

Teaching IT at Business Academy Aarhus. Formerly at Chainalysis, JADBio, Google, QIAGEN, CLC bio, Systematic, U Aarhus. PhD in computer science 2003.