4.
Understanding Widgets
Written by Vincent Ngo
You may have heard that everything in Flutter is a widget. While that might not be absolutely true, most of the time when you’re building apps, you only see the top layer: widgets. In this chapter, you’ll dive into widget theory. You’ll explore:
- Widgets
- Widget rendering
- Flutter Inspector
- Types of widgets
- Widget lifecycle
It’s time to jump in!
Note: This chapter is mostly theoretical. You’ll make just a few code changes to the project near the end of the chapter.
What is a widget?
A widget is a building block for your user interface. Using widgets is like combining Legos. Like Legos, you can mix and match widgets to create something amazing.
Flutter’s declarative nature makes it super easy to build a UI with widgets. A widget is a blueprint for displaying your app state.
You can think of widgets as a function of UI. Given a state, the build()
method of a widget constructs the widget UI.
Unboxing Card2
In the previous chapter, you created three recipe cards. Now, you’ll look in more detail at the widgets that compose Card2:
Do you remember which widgets you needed to build this card?
Recall that the card consists of the following:
- Container widget: Styles, decorates and positions widgets.
- Column widget: Displays other widgets vertically.
- AuthorCard custom widget: Displays the author’s information.
- Expanded widget: Uses a widget to fill the remaining space.
- Stack widget: Places widgets on top of each other.
- Positioned widget: Controls a widget’s position in the stack.
Widget trees
Every widget contains a build()
method. In this method, you create a UI composition by nesting widgets within other widgets. This forms a tree-like data structure. Each widget can contain other widgets, commonly called children. Below is a visualization of Card2’s widget tree:
You can also break down AuthorCard
and Expanded
:
The widget tree provides a blueprint that describes how you want to lay out your UI. The framework traverses the nodes in the tree and calls each build()
method to compose your entire UI.
Rendering widgets
In Chapter 1, “Getting Started”, you learned that Flutter’s architecture contains three layers:
In this chapter, you’ll focus on the framework layer. You can break this layer into four parts:
- Material and Cupertino are UI control libraries built on top of the widget layer. They make your UI look and feel like Android and iOS apps, respectively.
- The Widgets layer is a composition abstraction on widgets. It contains all the primitive classes needed to create UI controls. Check out the official documentation here: https://api.flutter.dev/flutter/widgets/widgets-library.html.
- The Rendering layer is a layout abstraction that draws and handles the widget’s layout. Imagine having to recompute every widget’s coordinates and frames manually. Yuck!
- Foundation, also known as the dart:ui layer, contains core libraries that handle animation, painting and gestures.
Three trees
Flutter’s framework actually manages not one, but three trees in parallel:
- Widget Tree
- Element Tree
- RenderObject Tree
Here’s how a single widget works under the hood:
- Widget: The public API or blueprint for the framework. Developers usually just deal with composing widgets.
- Element: Manages a widget and a widget’s render object. For every widget instance in the tree, there is a corresponding element.
- RenderObject: Responsible for drawing and laying out a specific widget instance. Also handles user interactions, like hit-testing and gestures.
Types of elements
There are two types of elements:
- ComponentElement: A type of element that’s composed of other elements. This corresponds to composing widgets inside other widgets.
- RenderObjectElement: A type of element that holds a render object.
You can think of ComponentElement as a group of elements, and RenderObjectElement as a single element. Remember that each element contains a render object to perform widget painting, layout and hit testing.
Example trees for Card2
The image below shows an example of the three trees for the Card2 UI:
As you saw in previous chapters, Flutter starts to build your app by calling runApp()
. Every widget’s build()
method then composes a subtree of widgets. For each widget in the widget tree, Flutter creates a corresponding element.
The element tree manages each widget instance and associates a render object to tell the framework how to render a particular widget.
Note: For more details on Flutter widget rendering, check out the Flutter team’s talk they gave in China on how to render widgets: https://youtu.be/996ZgFRENMs.
Getting started
Open the starter project in Android Studio, run flutter pub get
if necessary, then run the app. You’ll see the Fooderlich app from the previous chapter:
Next, open DevTools by tapping the blue Dart icon, as shown below:
DevTools will open in your browser. Select a widget on the left to see its layout on the right.
Note: It works best with the Google Chrome web browser. Click the ⚙ icon to switch between dark and light mode!
DevTools overview
DevTools provides all kinds of awesome tools to help you debug your Flutter app. These include:
- Flutter Inspector: Used to explore and debug the widget tree.
- Performance: Allows you to analyze Flutter frame charts, timeline events and CPU profiler.
- CPU Profiler: Allows you to record and profile your Flutter app session.
- Memory: Shows how objects in Dart are allocated, which helps find memory leaks.
- Debugger: Supports breakpoints and variable inspection on the call stack. Also allows you to step through code right within DevTools.
- Network: Allows you to inspect HTTP, HTTPS and web socket traffic within your Flutter app.
- Logging: Displays events fired on the Dart runtime and app-level log events.
- App Size: Helps you analyze your total app size.
There are many different tools to play with, but in this chapter, you’ll only look at the Flutter Inspector. For information about how the other tools work, check out: https://flutter.dev/docs/development/tools/devtools/overview.
Flutter Inspector
The Flutter Inspector has four key benefits. It helps you:
- Visualize your widget tree.
- Inspect the properties of a specific widget in the tree.
- Experiment with different layout configurations using the Layout Explorer.
- Enable slow animation to show how your transitions look.
Flutter Inspector tools
Here are some of the important tools to use with the Flutter Inspector.
- Select Widget Mode: When enabled, this allows you to tap a particular widget on a device or simulator to inspect its properties.
Clicking any element in the widget tree also highlights the widget on the device and jumps to the exact line of code. How cool is that!
- Refresh Tree: Simply reloads the current widget’s info.
- Slow Animation: Slows down the animation so you can visually inspect the UI transitions.
- Show Guidelines: Shows visual debugging hints. That allows you to check the borders, paddings and alignment of your widgets.
Here’s a screenshot of how it looks on a device:
-
Show Baselines: When enabled, this tells
RenderBox
to paint a line under each text’s baseline.
Here, you can see the green line under the baseline of each Text
widget:
- Highlight Repaints: Adds a random border to a widget every time Flutter repaints it. This is useful if you want to find unnecessary repaints.
If you feel bored, you can spice things up by enabling disco mode, as shown below:
- Highlight Oversized Images: Tells you which images in your app are oversized.
If an image is oversized it will invert the image’s colors and flip it upside down. As shown below:
Inspecting the widget tree
In the emulator, select the first tab, then click Refresh Tree in the DevTools. Finally, select Card1
and click Widget Details Tree tab, as shown below:
Note that:
- In the left panel, there’s a portion of the Flutter widget tree under investigation, starting from the root.
- When you tap a specific widget in the tree, you can inspect its sub-tree, as shown in the Widget Details Tree tab on the right panel.
- The Details Tree represents the element tree and displays all the important properties that make up the widget. Notice that it references renderObject.
The Details Tree is a great way for you to inspect and experiment with how a specific widget property works.
Click a Text
widget and you’ll see all the properties you can configure:
How useful is this? You can examine all the properties, and if something doesn’t make sense, you can pull up the Flutter widget documentation to read more about that property!
Inspecting like a pro
Besides checking the properties in Details Tree, you can evaluate your widgets in two other ways:
- Hover over any widget and it will show a pop-up with all the properties.
- Click on a widget to print the widget’s object, properties and state in the console.
As shown below:
Layout Explorer
Next, click the Layout Explorer tab, as shown below:
You can use the Layout Explorer to visualize how your Text
widget is laid out within the Stack
.
Next, follow these instructions:
- Make sure your device is running and DevTools is open in your browser.
- Click Card3 in the bottom navigation bar.
- Click the Refresh Tree button.
- Select the Column element in the tree.
- Click Layout Explorer.
You’ll see the following:
The Layout Explorer is handy for modifying flex widget layouts in real time.
The explorer supports modifying:
mainAxisAlignment
crossAxisAlignment
flex
Click start within the Main Axis and change the value to end. Notice that the Recipe Trends text is now at the bottom of the card:
This is useful when you need to inspect and tweak layouts at runtime. Feel free to experiment and play around with the Layout Explorer. You can create simple column or row widgets to mess around with the layout axis.
You now have all the tools you need to debug widgets! In the next section, you’ll learn about the types of widgets and when to use them.
Types of widgets
There are three major types of widgets: Stateless, Stateful and Inherited. All widgets are immutable but some have state attached to them using their element. You’ll learn more about the differences between these next.
Stateless widgets
You can’t alter the state or properties of Stateless widget once it’s built. When your properties don’t need to change over time, it’s generally a good idea to start with a stateless widget.
The lifecycle of a stateless widget starts with a constructor, which you can pass parameters to, and a build()
method, which you override. The visual description of the widget is determined by the build()
method.
The following events trigger this kind of widget to update:
- The widget is inserted into the widget tree for the first time.
- The state of a dependency or inherited widget — ancestor nodes — changes.
Stateful widgets
Stateful widgets preserve state, which is useful when parts of your UI need to change dynamically.
For example, one good time to use a stateful widget is when a user taps a Favorite button to toggle a simple Boolean value on and off.
Stateful widgets store their mutable state in a separate State
class. That’s why every stateful widget must override and implement createState()
.
Next, take a look at the stateful widget’s lifecycle.
State object lifecycle
Every widget’s build()
method takes a BuildContext
as an argument. The build context tells you where you are in the tree of widgets. You can access the element for any widget through the BuildContext
. Later, you’ll see why the build context is important, especially for accessing state information from parent widgets.
Now, take a closer look at the lifecycle:
- When you assign the build context to the widget, an internal flag,
mounted
, is set totrue
. This lets the framework know that this widget is currently on the widget tree. -
initState()
is the first method called after a widget is created. This is similar toonCreate()
in Android orviewDidLoad()
in iOS. - The first time the framework builds a widget, it calls
didChangeDependencies()
afterinitState()
. It might calldidChangeDependencies()
again if your state object depends on an inherited widget that has changed. There is more on inherited widgets below. - Finally, the framework calls
build()
afterdidChangeDependencies()
. This function is the most important for developers because it’s called every time a widget needs rendering. Every widget in the tree triggers abuild()
method recursively, so this operation has to be very fast.
Note: You should always perform heavy computational functions asynchronously and store their results as part of the state for later use with the
build()
function.build()
should never do anything that’s computationally demanding. This is similar to how you think of the iOS or Android main thread. For example, you should never make a network call that stalls the UI rendering.
- The framework calls
didUpdateWidget(_)
when a parent widget makes a change or needs to redraw the UI. When that happens, you’ll get theoldWidget
instance as a parameter so you can compare it with your current widget and do any additional logic. - Whenever you want to modify the state in your widget, you call
setState()
. The framework then marks the widget asdirty
and triggers abuild()
again.
Note: Asynchronous code should always check if the
mounted
property is true before callingsetstate()
, because the widget may no longer be part of the widget tree.
- When you remove the object from the tree, the framework calls
deactivate()
. The framework can, in some cases, reinsert the state object into another part of the tree. - The framework calls
dispose()
when you permanently remove the object and its state from the tree. This method is very important because you’ll need it to handle memory cleanup, such as unsubscribing streams and disposing of animations or controllers.
The rule of thumb for dispose()
is to check any properties you define in your state and make sure you’ve disposed of them properly.
Adding stateful widgets
Card2
is currently a StatelessWidget
. Notice that the Heart button on the top-right currently only displays a SnackBar()
, but nothing else like turning a solid color like a typical Favorite button. This isn’t because you haven’t hooked up any actions. It’s because the widget, as it is, can’t manage state dynamically. To fix this, you’ll change this card into a StatefulWidget
.
AuthorCard
is nested within Card2
. Open author_card.dart and right-click on AuthorCard
. Then click Show Context Actions from the menu that pops up:
Select Convert to StatefulWidget. Instead of converting manually, you can just use this menu shortcut to do it automatically:
There are now two classes:
class AuthorCard extends StatefulWidget {
...
@override
State<AuthorCard> createState() => _AuthorCardState();
}
class _AuthorCardState extends State<AuthorCard> {
@override
Widget build(BuildContext context) {
...
}
A couple of things to notice in the code above:
- The refactor converted
AuthorCard
from aStatelessWidget
into aStatefulWidget
. It added acreateState()
implementation. - The refactor also created the
_AuthorCardState
state class. It stores mutable data that can change over the lifetime of the widget.
Implementing favorites
In _AuthorCardState
, add the following property right after the class declaration:
bool _isFavorited = false;
Now that you’ve created a new state, you need to manage it. Replace the current IconButton
in _AuthorCardState
with the following:
IconButton(
// 1
icon: Icon(_isFavorited ? Icons.favorite : Icons.favorite_border),
iconSize: 30,
// 2
color: Colors.red[400],
onPressed: () {
// 3
setState(() {
_isFavorited = !_isFavorited;
});
},
),
Here’s how the new state works:
- First, it checks if the user has favorited this recipe card. If
true
, it shows a filled heart. Iffalse
, it shows an outlined heart. - It changes the color to red to give the app more life.
- When the user presses the
IconButton
, it toggles the_isFavorited
state via a call tosetState()
.
Save the change to trigger a hot reload and see the heart button toggle on and off when you tap it, as shown below:
Examining the widget tree
Now that you’ve turned AuthorCard
into a stateful widget, your next step is to look at how the element tree manages state changes.
Recall that the framework will construct the widget tree and, for every widget instance, create an element object. The element, in this case, is a StatefulElement
and it manages the state object, as shown below:
When the user taps the heart button, setState()
runs and toggles _isFavorited
to true. Internally, the state object marks this element as dirty. That triggers a call to build()
.
This is where the element object shows its strength. It removes the old widget and replaces it with a new instance of Icon
that contains the filled heart icon.
Rather than reconstructing the whole tree, the framework only updates the widgets that need to be changed. It walks down the tree hierarchy and checks for what’s changed. It reuses everything else.
Now, what happens when you need to access data from some other widget, located elsewhere in the hierarchy? You use inherited widgets.
Inherited widgets
Inherited widgets let you access state information from the parent elements in the tree hierarchy.
Imagine you have a piece of data way up in the widget tree that you want to access. One solution is to pass the data down as a parameter on each nested widget — but that quickly becomes annoying and cumbersome.
Wouldn’t it be great if there was a centralized way to access such data?
That’s where inherited widgets come in! By adding an inherited widget in your tree, you can reference the data from any of its descendants. This is known as lifting state up.
For example, you use an inherited widget when:
- Accessing a
Theme
object to change the UI’s appearance. - Calling an API service object to fetch data from the web.
- Subscribing to streams to update the UI according to the data received.
Inherited widgets are an advanced topic. You’ll learn more about them in Section 4, “Networking, Persistence and State”, which covers state management and the Provider package—a wrapper around an inherited widget.
Key points
- Flutter maintains three trees in parallel: the
Widget
,Element
andRenderObject
trees. - A Flutter app is performant because it maintains its structure and only updates the widgets that need redrawing.
- The Flutter Inspector is a useful tool to debug, experiment with and inspect a widget tree.
- You should always start by creating
StatelessWidget
s and only useStatefulWidget
s when you need to manage and maintain the state of your widget. - Inherited widgets are a good solution to access state from the top of the tree.
Where to go from here?
If you want to learn more theory about how widgets work, check out the following links:
-
Detailed architectural overview of Flutter and widgets: https://flutter.dev/docs/resources/architectural-overview.
-
The Flutter team created a YouTube series explaining widgets under the hood: https://www.youtube.com/playlist?list=PLjxrf2q8roU2HdJQDjJzOeO6J3FoFLWr2.
-
The Flutter team gave a talk in China on how to render widgets: https://youtu.be/996ZgFRENMs.
In the next chapter, you’ll get back to more practical concerns and see how to create scrollable widgets.