Building a Widget

Part 1 in my “Flutter from a complete beginner” series.

Tiger Asks...
12 min readJan 22, 2024

Tiger asks … what is Flutter?

I do believe that the most important thing to learn anything is to use it. But a certain basic level of understanding, I think, is helpful to put you in the correct mindset before you try to hack your first application.

Because if there’s one thing I hate when learning something new is not understanding why I’m doing it in a certain way.
As such, I personally like to frontload the dive into the concepts rather than delaying it until later, s.t. I can put whatever I learn into its proper context.

One thing in particular that I’ve realised is that the flutter keys are really central to understanding the lifecycle of a flutter widget and the element it represents.

So in this post, let’s try to make sense of the documentation and the various bits of information floating around in and outside of it, and understand how Flutter builds stuff.

The Flutter Runtime

Directly from the documentation:

Flutter apps run in a VM that offers stateful hot reload of changes without needing a full recompile.

Flutter apps are compiled directly to machine code, whether Intel x64 or ARM instructions, or to JavaScript if targeting the web.

“Flutter” mainly consists of three building blocks:

  • the Flutter Framework, that offers high-level access to the Flutter API and “composites” the app’s “widget” tree into a “scene”
  • the Flutter Engine, written in C++, provides the implementations for the Flutter APIs, as well as a Dart Runtime, and “rasterizes” scenes composited by the framework
  • an Embedder which is a platform-specific piece of code that manages the event loop and exposes platform-specific APIs to run the application

Interacting with “Flutter”, we have

  • a Runner, which is a bit of generated application boilerplate code that uses the APIs exposed by the Embedder to generate an actual runnable
  • the Dart App, which implements business logic and defines the widget tree

This is how flutter understands the vocabulary in quotation marks:

  • Widget: what some other UI frameworks call a “component”, a unit of UI logic, arranged in a tree
  • Compositioning: overlaying visual elements in draw order, the result is called a scene
  • Rasterizing: translating composition into GPU instructions that then render the scene

The widget

A widget is an immutable declaration of part of the user interface.

Everything is a widget

And they do mean everything. Even basic features like alignment or padding. For example, you don’t “align a widget”, you wrap it into an appropriate Align widget.

The documentation encourages you to have a look at Flutter’s Container widget as an example of how small, simple widgets can be combined to provide more complex functionalities.

The build() function

The UI-definition of a Widget takes place in an overriddenbuild() function, which maps a BuildContext instance to a Widget instance.

build()should be side-effect-free for performance reasons. I.e. no calculations, filtering, etc. Just take the data that’s there and display it.

heavy computational work should be done in some asynchronous manner and then stored as part of the state to be used by a build method

That is because build() is designed to be called often, with the framework deciding on its own which widgets’ build() functions need be called again to re-render only the parts of the UI that have a changed state. In particular, the documentation mentions the possibility of a widget’s build() function’s being called as often as once per rendered frame.

build() should also return a new tree of widgets every time it’s called, rather than trying to re-use old instances. A statement that Flutter itself immediately weakens in a footnote:

While the build function returns a fresh tree, you only need to return something different if there’s some new configuration to incorporate. If the configuration is in fact the same, you can just return the same widget.

Ok, that’s a bit confusing to me. So do we always return a new widget tree, keep a hold of the old ones “unless they changed”?

I think the point the documentation is trying to make is:

  • it’s ok to just return this instead of rebuilding the widget subtree if nothing in the subtree actually needs to change
  • do not keep track of parts of the subtree, rebuild the entire widget subtree and let Flutter figure it out (see also the “Performance Considerations” paragraph in the “State Management” section below)

The combination of these two should let each widget in the subtree decide on its own whether it needs to rebuild its subtree or not.

Flutter does mention the ability to move widgets as part of the build process rather than re-rendering them, in what they call non-local tree mutations, but I believe that part is better understood after we’ve talked about state management and the flutter build circle.

State Management

Having state

A StatelessWidget only depends on its own configuration information. It does not change with time, user interaction, etc.

class Frog extends StatelessWidget {
const Frog({
super.key,
this.color = const Color(0xFF2DBD3A),
this.child,
});

final Color color;
final Widget? child;
@override
Widget build(BuildContext context) {
return ColoredBox(color: color, child: child);
}
}

A StatefulWidget , on the other hand, has properties that can be changed externally, such as by user interaction.

class Bird extends StatefulWidget {
const Bird({
super.key,
this.color = const Color(0xFFFFE306),
this.child,
});

final Color color;
final Widget? child;
@override
State<Bird> createState() => _BirdState();
}
class _BirdState extends State<Bird> {
double _size = 1.0;
void grow() {
setState(() { _size += 0.1; });
}
@override
Widget build(BuildContext context) {
return Container(
color: widget.color,
transform: Matrix4.diagonal3Values(_size, _size, 1.0),
child: widget.child,
);
}
}

Notice, though, how the StatefulWidget is itself immutable. The mutable parts have been extracted into a State<Bird> object. This is important because it implies that a parent needs not be aware of whether its children are stateful or stateless, it can just recreate stateful widgets whenever it needs to and the framework will figure out which states to keep. Keep this also in mind because it’s rather important for when we later talk about flutter keys.

The createState() function is called whenever flutter inserts a widget instance into the element tree. (Something flutter calls “inflating a widget”.)
In particular, removing a stateful widget from a tree and inserting it later will create a new state for it (unless, again, it’s been assigned a GlobalKey).

As is the case with the grow() function in the example code, functions modifying the state need to call setState() to let the framework know that it needs to call the State's build() function again to reflect the changes in the UI.

Passing State

Passsing state as constructor argument(s) to children works, but as you can imagine very quickly becomes very cumbersome and annoying.

InheritedWidgets are flutter’s built-in IoC containers, intended to more efficiently pass state changes up and down a subtree. Instead of receiving a state object and using it, a widget will ask its BuildContext to provide the state object closest up in the widget tree.

class FrogColor extends InheritedWidget {
const FrogColor({
super.key,
required this.color,
required super.child,
});

final Color color;
static FrogColor? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<FrogColor>();
}
static FrogColor of(BuildContext context) {
final FrogColor? result = maybeOf(context);
assert(result != null, 'No FrogColor found in context');
return result!;
}
@override
bool updateShouldNotify(FrogColor oldWidget) => color != oldWidget.color;
}

One thing to note is that anInheritedWidget is immutable. I.e. a child widget cannot reassign its properties. But, the InheritedWidget could provide a service object or a property holder object, the instances of which would be final but their internal states would not need to be.

Another thing to note is that we can only access an InheritedWidget from its children’s context. Wrapping them in Builder instances may be necessary, at times.

class MyPage extends StatelessWidget {
const MyPage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(
body: FrogColor(
color: Colors.green,
child: Builder(
builder: (BuildContext innerContext) {
return Text(
'Hello Frog',
style: TextStyle(color: FrogColor.of(innerContext).color),
);
},
),
),
);
}
}

There’s some draw-backs with relying solely on InheritedWidget as a “dependency injection” mechanism (cf. https://medium.com/flutter-community/dependency-injection-in-flutter-with-inheritedwidget-b48ca63e823 ), but it’s definitely worth understanding how these work.

There’s also InheritedModel if you only want to depend on parts of the widget.

Provider is a wrapper around InheritedWidget aimed at making them easier to use. For my first app, this should be good enough, and it comes with a handy little tutorial for me to look at: https://docs.flutter.dev/data-and-backend/state-mgmt/simple

Riverpod ( https://riverpod.dev/ ) is the next step up from providers. It extends the providers concept, makes it flutter-independent and adds reactive caching. I don’t think I will yet look more closely into Riverpod, but when I do, this should probably be a good entry point: https://codewithandrea.com/articles/flutter-state-management-riverpod/

Bloc ( https://bloclibrary.dev/ ) goes a different route, opting instead for event-stream-based state handling. Unlike Riverpod, this one nests itself into Flutter. I’ve not had that close a look at Bloc, but its documentation has been lauded as being quite good.

(cf. https://otakoyi.software/blog/riverpod-and-bloc-packages-comparison for a comparison between Riverpod and Bloc)

Performance Considerations

We consider a StatefulWidget to be one that updates if it either calls State.setState or depends on at least one InheritedWidget (and in so doing gets rebuilt when they notify a change).

StatefulWidgets that never update (e.g. they may allocate resources on State.initState and dispose of them on State.dispose) are cheap, and can therefore have a more complex build() functions.

StatefulWidgets that do update are rebuilt many times and as such, building them must be a low-impact computation. Flutter recommends a bunch of techniques that can be summarised as follows:

  • keep your stateful widgets as small as possible, extract mutable parts into widgets of their own, if it helps achieve that
  • push your stateful widgets down the widget tree, they should ideally be leaves but affect as few child nodes as possible if they can’t be
  • use const widgets as much as possible to let flutter cache widget instances you know aren’t going to change (cf. https://medium.com/@Ruben.Aster/better-performance-with-const-widgets-in-flutter-50d60d9fe482 )
  • prefer not to conditionally wrap children into another widget or not, wrap and control the wrapper instead, to keep the subtree height constant on rebuild (the reason for this point should become clearer when we talk about keys later)
  • prefer widgets over helper functions s.t. flutter can take advantage of its caching capabilities

Flutter build pipeline

Now that we know what flutter is, how it’s composed of widgets and how it’s handling state updates, let’s have a look at how it all fits together.

To borrow a picture from Flutter themselves:

Fig.1: the render pipeline as described by Flutter

Build Phase

Everything is a widget, everything you see on screen is a render object.

A widget subtree represents a tree of elements that in turn abstract actual screen objects. In the build phase, Flutter maintains all of these trees and updates them frame to frame.

Fig.2: the different trees maintained by flutter

One key take-away is that while widgets are immutable (i.e. any change in a widget will re-create its entire sub-tree with all-new objects), the represented elements can often be re-used, and the render objects repainted.

And central to this re-usability are keys. We’ll go into keys a bit later, but suffice to say for now that removing a widget from the widget tree detaches the corresponding elements and render objects from the element and render trees, respectively. While the widget is replaced, the element and render object may end up being moved to a different position in their trees for re-use.

Layout Phase

Flutter layouting is a bit different from html layouting. One rule is central:

Constraints go down. Sizes go up. Parent sets position.

That is, a widget gets told how big it is allowed to be (in terms of min and max widths and heights), and it responds with how big — within those constraints — it wants to be.

(Note: the existence of e.g. the LayoutBuilder widget makes it useful to speak of “widgets” here, but it’s more accurate to say that this all happens on the render tree and for RenderObjects.)

To answer how large it wants to be, the widget asks each of its children in turn how large they want to be, given the constraints, and then arranges them along the x and y axis.

All of this is a single-pass process, essentially resulting in a depth-first traversal of the widget tree.

Implications:

  • a widget’s size is constrained by the parent
  • a widget can neither know nor set its position on the screen
  • position and size can only be defined in the context of the entire widget tree
  • use a LayoutBuilder widget if the widget’s build()function depends one the available constraints

Painting Phase

Once everything is lain out, painting then actually visually represents the scene on the canvas by calling all RenderObjects’ paint method.

It may be useful to know that we could hook into this process by subclassing the RenderObject and overriding said method. But at the same time, we probably won’t need to for most applications.

For custom painting, there are widgets that can be used instead. The how is a bit out of scope for this article, but I found one article by Bhimani and Sorathiya that seems to cover it nicely: https://www.dhiwise.com/post/bring-your-designs-to-life-with-flutter-painting-widgets

Composition Phase

Flutter actually maintains a fourth tree of objects, called the layer tree, that gives an order to the different painting instructions.

Painting doesn’t actually happen on one canvas, it happens on layers. That’s how we can get a scrolling something behind an unchanging menu bar, for example.

Compositioning now combines all of these different layers into a single picture, with lower layers of the layer tree painted last.

Even though each layer is painted separately, Flutter does try to group multiple RenderObjects into the same layer to minimise the amount of layers it needs to paint. It can therefore be useful to insert a RepaintBoundary widget to force a widget into a layer of its own to avoid unnecessary repainting.

Several framework widgets insert RepaintBoundary widgets to mark natural separation points in applications.

But it’s useful to keep an eye on the amount of repainting being done anyway.

Rasterization

The image so obtained is then handed over to the Flutter Engine to be turned into actual pixels on the screen.

Pixels that do not change do not need to be updated.

Flutter Keys

What are keys?

I earlier mentioned non-local tree mutations, or the ability to move widgets around instead of recreating them. We are now in a position where we can revisit that topic and understand it.

In particular, keep in mind that Flutter maintains an element tree that it updates during the build process based on the a rebuilt widget tree.

Keys, in a nutshell, are identifiers for widgets.

When a widget is rebuilt, Flutter compares the old widget subtree to the new.

  • a new widget matching the old widget’s (runtime type, key) pair, updates the underlying element (calls its Element.update with the new widget)
  • otherwise, the old element is removed from the element tree, the new widget is inflated into an element, and the new element is inserted into the element tree

This has performance implications, of course, with the general assumption being that updating an element should be quicker than reconstructing it.

Inflating a stateful widget into new element also will, if you remember, create a new state for that widget, discarding the old one.

Global keys allow a widget subtree to be moved rather than reconstructed:

When a new widget is found (its key and type do not match a previous widget in the same location), but there was a widget with that same global key elsewhere in the tree in the previous frame, then that widget’s element is moved to the new location.

When to use keys?

Dont. Unless you find your self adding, removing or reordering stateful widgets of the same type.

I found this video linked in the flutter documentation extremely helpful.It gives two examples:

  1. In a TODO-list, where a user may want to re-order items manually and done items should disappear from the list. In that case, giving each item a key allows them to be moved without having to reconstruct them.
  2. multiple children of the same type switch position. here, the key is needed to correctly attach the state to the widget.

Both of these examples can be handled with local keys if the widgets with keys have the same parent (i.e. “are on the same level”) in the widget tree.

That is because Flutter only considers local keys at the same level in the widget tree when trying to map old elements to new widgets.

But with some careful placement of the keys, local keys are still sufficient.

So what about global keys? The documentation warns:

Reparenting an Element using a global key is relatively expensive

so it’s not entirely surprising that the general advise is to avoid it. Mentioned use cases include

  1. showing the same widget (with the same state) in two different locations (that aren’t displayed at the same time)
  2. hiding state from the widget tree

In either case, if you feel the need to use a global key, reconsider if there isn’t a better way to go about it that avoids them.

--

--

Tiger Asks...

🇨🇭-based Software Engineer with a lot of questions and some answers.