Chapters

Hide chapters

Flutter Apprentice

Third Edition · Flutter 3.3 · Dart 2.18.0 · Android Studio 2021.2.1

Section IV: Networking, Persistence and State

Section 4: 7 chapters
Show chapters Hide chapters

Appendices

Section 7: 2 chapters
Show chapters Hide chapters

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.

UI = f Screen Build ( 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:

Column Container Center Card2 AuthorCard Expanded Root Widget build() build() build() build() build()

You can also break down AuthorCard and Expanded:

Row IconButton build() build() build() build() CircleImage SizedBox Column Text Text build() build() Positioned Positioned Text RotatedBox Text build() build() build() build() build() AuthorCard Container Row Stack 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:

Embedder (Platform-specific) Framework (Dart) Engine (C/C++)

In this chapter, you’ll focus on the framework layer. You can break this layer into four parts:

Foundation Rendering Widgets Material or Cupertino

  • 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:

FooWidget Widgets Configuration Element Lifecycle management Hold properties Public API FooElement Manage references and element tree Represents a Widget in a tree RenderObject Paint Knows how to size and paint Layout Children Listen for input, hit-testing RenderFoo

  1. Widget: The public API or blueprint for the framework. Developers usually just deal with composing widgets.
  2. Element: Manages a widget and a widget’s render object. For every widget instance in the tree, there is a corresponding element.
  3. 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:

  1. ComponentElement: A type of element that’s composed of other elements. This corresponds to composing widgets inside other widgets.
  2. 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:

Widget Tree Element Tree RenderObject Tree Card2 Center Container Column AuthorCard Expanded StatelessElement StatelessElement StatelessElement StatelessElement RenderPositionedBox RenderDecoratedBox RenderFlex RenderObject

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:

  1. Hover over any widget and it will show a pop-up with all the properties.
  2. 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:

  1. Make sure your device is running and DevTools is open in your browser.
  2. Click Card3 in the bottom navigation bar.
  3. Click the Refresh Tree button.
  4. Select the Column element in the tree.
  5. 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.

Stateless Widgets Lifecycle build(context)() constructor

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:

  1. The widget is inserted into the widget tree for the first time.
  2. 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 Lifecycle createState() constructor

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.

didChangeDependencies initState dirty = true BuildContext assigned build dirty = false dispose deactivate didUpdateWidget setState oldWidget state changes mounted = true mounted = false widget.properties

Now, take a closer look at the lifecycle:

  1. When you assign the build context to the widget, an internal flag, mounted, is set to true. This lets the framework know that this widget is currently on the widget tree.
  2. initState() is the first method called after a widget is created. This is similar to onCreate() in Android or viewDidLoad() in iOS.
  3. The first time the framework builds a widget, it calls didChangeDependencies() after initState(). It might call didChangeDependencies() again if your state object depends on an inherited widget that has changed. There is more on inherited widgets below.
  4. Finally, the framework calls build() after didChangeDependencies(). This function is the most important for developers because it’s called every time a widget needs rendering. Every widget in the tree triggers a build() 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.

  1. The framework calls didUpdateWidget(_) when a parent widget makes a change or needs to redraw the UI. When that happens, you’ll get the oldWidget instance as a parameter so you can compare it with your current widget and do any additional logic.
  2. Whenever you want to modify the state in your widget, you call setState(). The framework then marks the widget as dirty and triggers a build() again.

Note: Asynchronous code should always check if the mounted property is true before calling setstate(), because the widget may no longer be part of the widget tree.

  1. 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.
  2. 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 a StatelessWidget into a StatefulWidget. It added a createState() 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:

  1. First, it checks if the user has favorited this recipe card. If true, it shows a filled heart. If false, it shows an outlined heart.
  2. It changes the color to red to give the app more life.
  3. When the user presses the IconButton, it toggles the _isFavorited state via a call to setState().

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:

AuthorCard Icon (♡) Widget Tree Element Tree Stateful Element Stateless Element State (_isFavorited = false)

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().

AuthorCard Widget Tree Element Tree Stateful Element Stateless Element State (_isFavorited = true) dirty! Icon (♥) new Icon widget instance Tap to favorite

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.

AuthorCard Widget Tree Element Tree Stateful Element Stateless Element State (_isFavorited = true) clean 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?

Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget Widget InheritedWidget Widget Widget Widget Scenario 1 Scenario 2 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 and RenderObject 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 StatelessWidgets and only use StatefulWidgets 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:

In the next chapter, you’ll get back to more practical concerns and see how to create scrollable widgets.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.