Flutter and Widget Internals
Flutter and Widget Internals
With your code, you control the widget tree, the other two trees are controlled internally by
Flutter but they are based on your widget tree. The widget tree which you're creating with your
code and which is built by Flutter calling the build method is essentially just a bunch of
configurations Flutter takes into account, it's not directly output on the screen or anything like
that, instead, it simply describes to Flutter what should be output on the screen. So it's just a
bunch of configuration settings you could say. The element tree then is essential because the
element tree is created by Flutter automatically based on your widget tree and it links your
widgets to the actual rendered objects.
The widget tree is constantly changing basically whenever you call to setState for example, so
whenever the build methods get executed, Flutter rebuilds that widget tree, whilst that happens,
the element tree is managed differently and does not rebuild with every call to the build method
and it links the widgets, the configuration you set up with the actually rendered elements which
are part of the render tree. So the render tree in the end you could say is the representation of
what really ends up on the screen, what you see on the screen, and that also is rarely rebuilt.
Now let's have a more concrete example.
Now for every widget, you have in the widget tree, Flutter automatically creates an element. It
does so when it first encounters that widget, so for example when the app starts or if you have an
if condition and some condition is now true and that means that now a new widget should be
rendered which hasn't been rendered before.
Whenever Flutter encounters a widget with no element yet, it creates an element. So an element,
in the end, is just an object you could say managed in memory by Flutter which holds a
reference at its widget. The element itself has no internal configuration, it really just points at
the widget which then holds the configuration.
So if you have a container with let's say a background color, the container element would indeed
just be a relatively empty box pointing at the widget which then holds the information about the
background color and so on.
Now such elements get created for all widgets. In the widget itself which extends the stateful
widget, you had the createState method which in the end is called by Flutter to create a new
state object based on your state class and then the object is connected to the element indirectly.
The important thing really is however that the element has a reference at the widget which holds
all the configuration for that element and then it also holds a reference to the state object which is
an independent object, not part of the widget or anything like that but an independent object
managed in memory. So these are the elements but if they're just basically empty boxes pointing
at a widget, what is their role? Well, they're not just pointing at a widget, they also point at the
rendered box, at the rendered object you see on the screen.
Whenever Flutter encounters an element that hasn't been rendered to the screen you could say, it
does render it to the screen and it does so by looking at the widget at which this element also
points which holds all the information you need for painting it to the screen, like a background
color, a border the size, such things. Flutter then paints that box to the screen.
So the element then has a pointer at the element on the screen, so at the object, on the screen, and
at the widget which holds the configuration, and of course every element has a rendered
equivalent, which kind of ends up on the screen.
As mentioned above, the setState method calls to build, the same is true if you use something
like MediaQuery.of() or theme.of() and will cause your widget to rebuild automatically
whenever the MediaQueryData changes. So for example when you rotate the device, the
MediaQueryData changes because orientation is information stored in the MediaQueryData, and
therefore, rotating your device automatically also triggers the build to run.
The same is true if the soft keyboard appears and viewInsets.bottom setting tells us how much
space is occupied by the soft keyboard.
In small apps, not really in a measurable way to be honest but especially in bigger apps,
every tiny bit of performance improvement can count and one relatively easy
improvement is the usage of so-called const constructors and const widgets.
As you learned earlier in the course, final variables or properties are variables or
properties which are created dynamically at runtime but which then never change. On the
other hand, const properties or variables are properties or variables that are holding a
certain value that is already known when the code compiles and which will never change
after compilation.
const Constructor
In the above code snippet, we have a constructor, and we can also add a const keyword in
front of that constructor as well as it showed in it. We can do this if we bind all your
arguments there in the constructor to the final properties, so for example if we turn that
the body property into a non-final, so if that body is not final, we get an error that we
can't make this a constructor for non-final fields.
But if we only have the final field which we’re targeting, so if we saying that whenever
we're creating an instance of this Content class, this instance will be unchangeable
because you won't be able to then access this instance with the dot notation and change
the body after this object has been created, then we can mark this as const which means
every instance of this object you create is immutable, we can't change it. It is clear that
our intention with this class is that every new object that's created based on it is
immutable. So it can't be changed, we can only change it by replacing it with a new
instance.
A widget tree is full of immutable objects, all the widgets are immutable. When the build
method runs, in the end, it creates a bunch of new instances because it calls the
constructors again, so then it replaces the old instances with the new ones, it does not go
ahead and take the old widget and only updates its body by assigning a new value,
instead, it creates a new widget with a new body passed into the constructor, that's what's
happening.
Now when we have const and we do indeed create an instance of this widget with
compile-time constant data like follow, then we can let Flutter know about it.
So here, we could then, when we use our immutable widget, we could add const in front
of it. Now in theory, in Flutter, every stateless widget we build is immutable and you can
add const in front of the constructor, of basically every stateless widget we build because,
in stateless widgets, we always have final unchangeable properties because we can't use
setState in here and update the UI. So hence, basically all our stateless widgets are
theoretically immutable or are immutable but don’t work always because the data we can
pass into a concrete object a dynamic content which not known at the point of time of
compilation.
Arguments of a constant creation must be constant expressions and this is not constant
because this value here changes dynamically.
So, why would we have wanted to add const here is of course the important question. we
can skip unnecessary widget rebuilds if you mark a widget as const. Of course, it's not
super costly to do so because it only happens in the widget tree and that is done very
efficiently but still, if we can skip one extra redundant object instantiation, we should do
it. So adding const here is a good idea because that tells Dart that the value after it will
never change and that tells Flutter that since this will never change, when it rebuilds the
widget tree, for this specific widget, it can simply take the old widget which was in the
same position, it doesn't need to recreate the object.
Widget Lifecycle
Stateful Widget Lifecycle
When a Flutter builds a StatefulWidget, it creates a State object. This object is where all the
mutable state for that widget is held.
2. mounted is true
When createState creates the state class, a buildContext is assigned to that state. A BuildContext
is, overly simplified, the place in the widget tree in which this widget is placed. All widgets
have a bool this.mounted property. It turns true when the buildContext is assigned. It is an error
to call setState when a widget is unmounted.
tip: This property is useful when a method on your state calls setState() but it isn't clear when or
how often that method will be called. Perhaps it is being called in response to a stream updating.
You can use if (mounted) {... to make sure the State exists before calling setState().
3. initState()
This is the first method called when the widget is created (after the class constructor, of course.)
initState is called once and only once. It must also call super.initState().
1. Initialize data that relies on the specific BuildContext for the created instance of the
widget.
2. Initialize properties that rely on this widgets 'parent' in the tree.
3. Subscribe to Streams, ChangeNotifiers, or any other object that could change the data on
this widget.
4. didChangeDependencies()
The didChangeDependencies method is called immediately after initState on the first time the
widget is built.
It will also be called whenever an object that this widget depends on data from is called. For
example, if it relies on an InheritedWidget, which updates.
build is always called after didChangeDependencies is called, so this is rarely needed. However,
this method is the first chance you have to call BuildContext.inheritFromWidgetOfExactType.
This essentially would make this State 'listen' to changes on a Widget it's inheriting data from.
The docs also suggest that it could be useful if you need to do network calls (or any other
expensive action) when an InheritedWidget updates.
5. build()
This method is called often (think fps + render). It is a required, @override, and must return a
Widget.
Remember that in Flutter all GUI is a widget with a child or children, even 'Padding', 'Center'.
6. didUpdateWidget(Widget oldWidget)
didUpdateWidget() is called if the parent widget changes and has to rebuild this widget (because
it needs to give it different data), but it's being rebuilt with the same runtime type, then this
method is called.
This is because Flutter is re-using the state, which is long-lived. In this case, the required is to
initialize some data again, as one would in initState().
If the state's build() method relies on a Stream or other object that can change, unsubscribe from
the old object and re-subscribe to the new instance in didUpdateWidget()
Flutter is always called build() after this, so any subsequent further calls to setState are
redundant.
setState()
The 'setState()' method is called often from the Flutter framework itself and from the developer.
It is used to notify the framework that "data has changed", and the widget at this build context
should be rebuilt.
setState() takes a callback that cannot be async. It is, for this reason, it can be called often as
required because repainting is cheap :-)
8. deactivate()
This is rarely used. 'deactivate()' is called when State is removed from the tree, but it might be
reinserted before the current frame change is finished. This method exists basically because State
objects can be moved from one point in a tree to another.
9. dispose()
'dispose()' is called when the State object is removed, which is permanent. This method is where
to unsubscribe and cancel all animations, streams, etc.