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.

and parameters typed and . So the function cannot be passed to any of those. Of course, the result of invoking can. But then you would pass a widget tree pre-built in the current build context, rather than a that builds its subtree only if needed and in the context defined by wherever it ends up in the overall tree. Noticed the expression at the start of ? It retrieves the primary color from the nearest widget up the parent chain — which may well be different from the nearest at the point where 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.