Functional bits in Flutter — follow up

Mattia Maldini
Flutter Community
Published in
12 min readJan 17, 2019

A couple of months ago I wrote an article to showcase two different approaches — functional and object-oriented — in a simple yet practical example that occurred to me while working with Flutter. Specifically, the difference between defining a new StatelessWidget and composing one with a function.

I published it without giving it much thought. I later found out thanks to some comments that I wasn’t the first one to lucubrate on Flutter’s duality; this Github discussion basically covers the same topic, with much more respectable opinions.

Since it was so well received I felt inspired to do a small follow up, digging a little more into the differences between the functional and object-oriented strategies.

Let’s begin summarizing the conclusions; they are pretty obvious, but I’ve previously failed to convey them.

Flutter allows us, developer, to have a great deal of freedom in deciding whether to follow a functional or object-oriented route. This is a trait that more and more programming languages are adopting by adding new logical constructs either via libraries or native tools (to name one, anonymous functions). This has, however, always hit a hard limit, at least in my experience.

Although they might offer different options, there is always a “preferred choice” that gleams unmistakably. Dart has functional tools, but is an object-oriented language at heart: going against its nature means sacrificing something else.

It’s also the case for many others, even on the opposite side: elegant, purely functional languages like OCaml and Haskell technically have implementations for objects and classes, but using them somehow feels wrong.

What is the downside in the case of Dart and Flutter? Mostly performance. Interfaces in Flutter are meant to be built by subclassing Widgets and creating them through functions instead skips many of the optimizations the framework has to offer.

Note: My point in the previous article was purely stylistic. I argue that, from the code’s perspective, the functional result is more elegant and readable. Right now I’m going to dive in the practical advantages of using an object instead.

Const

A custom Widget can be declared to use a const constructor, which means every instance of said Widget will be compile-time defined, saving resources at runtime since there is no actual initialization code to be executed.

This isn’t a clean upside for StatelessWidgets because there are strict rules for defining a constant class. In my example the instance of UsefulDevice containing the data to be displayed should be constant as well because it’s passed to the constructor, but that simply isn’t possible because my data is not immutable.

Still, in certain situations being able to create everything during compilation will save time and memory. Using a function to create the Widget will, by definition, deny this advantage.

Navigator incoherence

This one is subtle. It might be the only real problem with using a function instead of a Widget, but I’m still not certain about its severity.

To be honest, I struggled quite a lot to even understand what the problem was and to reproduce an instance where it would arise.

Basically, Flutter internally keeps track of the tree made of Widgets that should be displayed on the screen; it expects said tree to be built with nested Widgets in an object-oriented fashion, and this transpires in various ways (this will be a recurring theme in this article).

Specifically, every Widget has an associated BuildContext, which is “a handle to the location of a widget in the widget tree”. It serves various purposes, like holding a reference to the current Navigator. To keep it short, the Navigator is a Widget that organizes other Widgets as a stack; it can be used, most notably, to navigate between different screens (Activities? ViewControllers? Pages? Still whatever).

When I use a function to spawn a bunch of Widgets instead of creating a new Widget and overriding the build method, I don’t have immediate access to the BuildContext that would be an argument for build .

In my example this wasn’t a problem: I didn’t need a BuildContext in my function. But what if I did? Well, I can always pass the context from the previous build method to the function, right?

Correct. But although it can work, this little hiccup in context managing was clearly not meant to be for Flutter, and I’d wager it can lead to small issues that require hours of painful debugging to find.

That being said, as of now I know of only one example: a context that was created before any MaterialApp and thus doesn’t know about the existence of any Navigator , as they are initially setup by the MaterialApp instance. In a single example,

import 'package:flutter/material.dart';void main() => runApp(Foo());class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: RaisedButton(
onPressed: () {
// Navigate back to first screen when tapped!
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
class Foo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//Whoops! This context was created before MaterialApp!
home: homeFunction(context),
);
}
}
Widget homeFunction(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(builder: (context) => SecondScreen()),
),
child: Text('Next Screen'),
),
),
);
}

Notice how homeFunction receives a context created before the MaterialApp . Pressing the button will result in Flutter complaining there is no Navigator for the provided context.

I/flutter ( 6813): ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
I/flutter ( 6813): The following assertion was thrown while handling a gesture:
I/flutter ( 6813): Navigator operation requested with a context that does not include a Navigator.
I/flutter ( 6813): The context used to push or pop routes from the Navigator must be that of a widget that is a
I/flutter ( 6813): descendant of a Navigator widget.

This is a problem. Wait, is it really? Not in my experience. Passing a context that existed before the MaterialApp is a really specific and rare situation. In the example I created for my previous article passing the current context to the buildMainContent function does not lead to any error because it’s created by the HomePage Widget, within the MaterialApp instance. Honestly, it’s so borderline I struggle to include it as an issue.

It is however a symptom of trying to use Flutter in a way it was not built to. You can do it, but it’s untested ground; on the other hand Widget are bulletproof in those aspects.

Hot Reload

I’m quite sure I’ve stumbled upon this one but at the time I didn’t realize it. Sometimes the hot reload function would miss a piece or otherwise something didn’t add up with the changes I had made, and the only solution was to close the app and reload it entirely.

Apparently, hot reload targets the Widget tree specifically; this means that functions are not guaranteed to be called again after a refresh. They most likely will be, as modifying something will probably lead to their call being fired again in the midst of a cascade effect; there are however some weird cases where it will not happen.

Take the above example, but slightly change the entry point for the app:

import 'package:flutter/material.dart';
void
main() => runApp(builder());
Widget builder() {
return MaterialApp(
// We have no context here... buttons won't work, but that's not the point
home: home(null),
);
}
...

This is … peculiar. Instead of instancing our app in the runApp call, we return a Widget that is semantically identical to the Foo Widget. Not a big deal, uh?

It is, apparently. If you try to change anything in the app’s content the hot reload function will not work. Seriously, try it out.

This is all because Flutter Widgets aren’t actually what appears on the screen. The Widget tree is more like a blueprint, a schematic for the structure of your GUI that is then used to build an Element tree; Flutter then uses the Widget tree as a reference to reload and rebuild the interface. When we use a function we cut that tree, blocking passage for the runtime and preventing the hot reload.

If the Widget bearing function is used in any place other that the runApp function it wouldn’t count due to the cascade effect mentioned above: if the Widget that calls the function gets rebuilt the function will be called again as well.

Probably won’t be an issue, but it might become one when you least expect it.

Less Rebuilds

For reasons that should be clear at this point, whenever you’re “cutting the tree” of Widgets with a function you are preventing some optimizations. Flutter is built on Widgets and issues might arise if you trim too much the tree it expects to find.

Imagine we have a tree with 3 levels of Widgets. We can cut the mid level and use a function instead. In the previous situation whenever there was a change at level 3 we could rebuild just that level; using the function, this is not an option anymore. The third level is not tracked by the Widget tree, so it only changes when carried along by the first level.

The purple zone is unmanaged by Flutter

This does not create actual errors or unexpected behaviors because the Widgets are Stateless anyway (remember: we can’t pack state into a function), so when something changes there are always the corresponding updates. For Flutter, this is a matter of optimization.

As with all performance concerns, your mileage may vary. If the UI is small and the widgets simple you will probably never face issues even by cutting the Widget tree like this. However the time may come when your application becomes less usable due to the performance blow, and the only solution is to implement a proper Widget structure that can be tracked down and optimized by the Flutter engine.

Debugging Completeness

Another small detail, but still a point in favor of Widgets. Widgets can overwrite the debugFillProperty to show more detailed information in the Widget inspector of your IDE of choice while debugging. Functions obviously lack this ability.

Naming Convention

I list this just for completeness’ sake, as it was mentioned in the original Github discussion. Imagine you decide to use a Widget-bearing function instead of declaring a new StatelessWidget. Abiding to the common OOP lintiing standards, your function name has its leading letter in lowercase — myWidget() opposed to MyWidget.

Now pretend something is changed and your Widget now needs to have an associated state. A function isn’t cut for it anymore as it cannot hold permanent values, so you need to switch to a StatefulWidget. You should then also change the name, capitalizing the first letter (MyWidget).

Technically this is a breaking change because you have to rename every call to that Widget… Except every editor has a find&replace functionality and even then you are not forced to capitalize the name (you really should though).

The only problematic situation I can think of is if you function is part of an exposed library interface (=changing its name will be bad for the userbase) and you are under a strict coding standard (=classes must have capitalized names). In that case, just create a new function with the same name and make it return the newly defined Widget.

Again, only a problem in very borderline scenarios.

What was my point again?

I’ve mentioned five potential sources of trouble when using functions instead of Widgets. The bottom line is, Flutter likes Widgets; it will not explicitly forbid the use of alternatives, but under the hood it won’t appreciate them as much, grumbling and complaining in an undertone.

Why did I ever use a function then? Well, to quote myself on the initially presented problem:

Since the ControlPanel will communicate with the device it must know everything about it: internal data, functions, and connection state; it must keep track of and modify something that cannot be directly accessed from the main page (they are in two different modules now), therefore the control panel must have a state on its own, copy of the needed data.

I had a parent Widget holding a state that needed to be accessed by a child, and I didn’t want to use a StatelessWidget to avoid passing around references through chains of constructors. I tried using a StatefulWidget , but got too annoyed by all the boilerplate code when adding a couple of callbacks, so I used a simple function.

Recap of the trial and error of the previous article

Again, from a stylistic point of view a function is vastly superior (and to be entirely honest with you, I still prefer the functional approach); let’s say however that we want to do things the Flutter way while still avoiding boilerplate code and bad programming practices. We have two options: using code generation techniques or subclassing InheritedWidget.

Code generation

This work sprouts mainly from the Github thread that was basically discussing this same topic. The OP was focused on the boilerplate code associated with StatelessWidget and was asking clarification on what is exactly the downside of using functions.

Having already covered that, what are we left with? Well, if it’s just a matter of writing less code, code snippets are enough. If you want to define a StatelessWidget in VS Code, just start typing stl and press tab; autocompletion will do the rest for you.

Unfortunately, it’s not so much about writing than about reading your code. This is a close up comparison of the two approaches:

The Widget packs a whooping 5-6 lines of code, while the function can work with just 1. If you don’t see how this has a huge impact on your code as a whole then I probably wouldn’t like working with you on the same programming project.

As it turns out, you can have the best of both world by using code generation: Remi Rousselet created a dart package exactly for this, called functional_widgets.

Simply put, it allows you to write a function but have a Widget by using a decorator. Write

@widget
Widget myFunctionalWidget(BuildContext context, String text) {
return Text(text);
}

And the framework will generate the following for us

class MyFunctionalWidget extends StatelessWidget {
final String text;

const MyFunctionalWidget(this.text, {Key key}) : super(key: key);

@override
Widget build(BuildContext context) {
return myFunctionalWidget(context, text);
}
}

Still, in my opinion, you can hardly consider this a solution (yes, I know I’m picky).

First of all, resorting to code generation means surrendering to the weakness of you language. Second, it’s not that convenient either: before using functional_widgets I needed to add three dependencies, two import, one part statement and run an external (albeit still within Flutter) code generator. All of this while still recognizing that Dart has a very flexible and convenient approach to code generation.

InheritedWidget

Let’s back up a little, and look at the root of my initial task. I wanted to send a message from a child to an ancestor in the Widget tree (“someone clicked me, disconnect from the current device!”). This boils down to the problem of sharing something between elements of the tree (in this case callbacks) without having to pass it down for every generation among the boilerplate code of classes and objects.

Well, after some more studying and practice I now know Flutter has a specific solution to this: the InheritedWidget class.

A custom InheritedWidget at the top of the tree can be found and referenced by all of its children, freeing them from the burden of divided state and parameters. Being a convenient tool to implement the “One Single Source of Truth” philosophy, it’s officially my new favorite thing about Flutter.

IneritedWidget allows to cut the line between you and your ancestors

In my example, I can pack all the state and a couple of callbacks in a subclass of InheritedWidget that becomes the root of the Widget tree. In the child Widget I access the InheritedWidget reference through the BuildContext and use the callbacks to send notifications to my parent Widget.

class StatelessControlPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
final state = AppCentralState.of(context);
return Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text("Connected"),
RaisedButton(
child: Text(state.device.onoff ? "ON" : "OFF"),
color: state.device.onoff ? Colors.green : Colors.red,
onPressed: () => state.updateStateCallback(!state.device.state),
),
FlatButton(
child: Text("Disconnect"),
onPressed: state.disconnectCallback,
),
],
);
}
}

I updated my Github contrived example to use this.

Now, as the last one of this far too long list of possible solution to a not-that-big-of-a-deal problem, I will surely be satisfied with this approach, won’t I?

Eeeeeehhh…. I don’t know.

Making functions accessible like that to every Widget in the tree… what happened to encapsulation? It feels a little too… global.

Still, it’s not exactly globally visible. You have to explicitly request the InheritedWidget instance and it can be limited to certain subtrees. I can live with that.

Conclusion

Functions are great, but Dart and Flutter prefer classes. It’s not that bad, and here are my very personal ratings of the possible solutions I found in this learning adventure to pass data along the Widget tree.

  • StatefulWidget : 1/10
  • StatelessWidget: 5/10
  • functional Widgets with code generation: 6/10
  • Widget-returning function: 8/10
  • StatelessWidget + InheritedWidget: 8/10

All of my remaining dissatisfaction is purely whimsical at this point. I’ll happily work with what Flutter has to offer me, appreciating the privilege of avoiding the same work under Java or C++.

--

--

Mattia Maldini
Flutter Community

Computer Science Master from Alma Mater Studiorum, Bologna; interested in a wide range of topics, from functional programming to embedded systems.