Chapters

Hide chapters

Flutter Apprentice

Fourth Edition · Flutter 3.16.9 · Dart 3.2.6 · Android Studio 2023.1.1

Section II: Everything’s a Widget

Section 2: 5 chapters
Show chapters Hide chapters

Section IV: Networking, Persistence & State

Section 4: 6 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 the state of your app.

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 CategoryCard

In the previous chapter, you created three cards. Now, you’ll look at the widgets that compose CategoryCard in more detail:

Do you remember which widgets you used to build this card?

Recall that the card consists of the following:

  • Card widget: A material design container that wraps the content.
  • Column widget: Organizes content vertically, with a Stack for images and texts and a ListTile for category details.
  • Stack widget: Overlays multiple widgets, used here to layer the image with two pieces of text.
  • ClipRRect widget: Provides rounded corners for the image.
  • Image widget: Loads the image.
  • Positioned widget: Positions “Yummy” and “Smoothies” texts over the image.
  • Text widget: Displays text from category details and static labels.
  • RotatedBox widget: Rotates the text by 90°.
  • ListTile widget: Displays the category’s name and number of associated restaurants.

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 CategoryCard’s widget tree:

Column Card CategoryCard build() build() build() Root Widget Stack build() ListTile build() Text Text build() ClipRRect build() Positioned Positioned Image build() build() Text RotatedBox Text

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

  • Widget: The public API or blueprint for the framework. Developers usually just deal with this layer.
  • 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 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 CategoryCard

The image below shows an example of the three trees for the CategoryCard widget:

Widget Tree Element Tree RenderObject Tree CategoryCard Card Column Stack ListView StatelessElement StatelessElement StatelessElement RenderObject 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. Flutter creates a corresponding element for each widget in the widget tree.

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, and then run the app. You’ll see the Yummy app from the previous chapter:

Next, open DevTools by tapping the blue Dart icon, as shown below:

DevTools will open in your browser.

Note: It works best with the Google Chrome web browser. Click the ⚙ icon to switch between dark and light mode.

Select a widget on the left to see its layout on the right.

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.

Depending on if your browser is expanded or collapsed, you’ll see one of the following toolsets.

Look at each tool to see how it can help you identify issues.

  • 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 your widgets’ borders, paddings and alignment.

Here’s a screenshot of how guidelines look 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’ll 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 CategoryCard 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’ll 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:

  1. Make sure your device is running, and DevTools is open in your browser.
  2. Click Post in the bottom navigation bar.
  3. Click the Refresh Tree button.
  4. Select the Row 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
  • fit

Click start within the Cross Axis and change the value to stretch. Notice that the PostCard widget stretches the entire screen:

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.

Learning the Types of Widgets

There are three major types of widgets: Stateless, Stateful and Inherited. All widgets are immutable, but some have a state attached to them using their element. You’ll learn more about the differences between these next.

Stateless Widgets

The state or properties of a stateless widget can’t be altered 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:

  • 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 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’s 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(). In some cases, the framework can 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

Wouldn’t it be great if your users could save their list of favorite restaurants to gain quick access to reorder again? You’ll add a heart button to RestaurantLandscapeCard for users to save a restaurant.

RestaurantLandscapeCard is currently a StatelessWidget, which means the widget can’t manage state dynamically. To fix this, you’ll change this card into a StatefulWidget.

Open restaurant_landscape_card.dart and right-click RestaurantLandscapeCard. 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 RestaurantLandscapeCard extends StatefulWidget {
  ...

  @override
  State<RestaurantLandscapeCard> createState() =>
    _RestaurantLandscapeCardState();
}

class _RestaurantLandscapeCardState extends State<RestaurantLandscapeCard> {
  // TODO: Add _isFavorited property
  @override
  Widget build(BuildContext context) {
    ...
  }

A couple of things to notice in the code above:

  • The refactor converted RestaurantLandscapeCard from a StatelessWidget into a StatefulWidget. It added a createState() implementation.
  • The refactor also created the _RestaurantLandscapeCardState state class. It stores mutable data that can change over the lifetime of the widget.

Implementing Favorites

In _RestaurantLandscapeCardState, find // TODO: Add _isFavorited property and replace it with the following property:

bool _isFavorited = false;

Now that you’ve created a new state, you need to manage it. Locate the comment // TODO: Convert to a stack and replace it and the whole child property below it with the following:

// 1
child: Stack(
  fit: StackFit.expand,
  children: [
    // 2
    Image.asset(
      widget.restaurant.imageUrl,
      fit: BoxFit.cover,
    ),
    // 3
    Positioned(
      top: 4.0,
      right: 4.0,
      child: IconButton(
        // 4
        icon: Icon(_isFavorited
            ? Icons.favorite  //
            : Icons.favorite_border,
          ),
        iconSize: 30.0,
        color: Colors.red[400],
        // 5
        onPressed: () {
          setState(() {
            _isFavorited = !_isFavorited;
          });
        },
      ),
    ),
  ],
)

Here’s how the code works:

  1. Stack widget overlays multiple elements. Here, it’s used to layer a favorite button over a restaurant’s image, ensuring you utilize the full space.
  2. The restaurant’s image is displayed, with a scaling set to fill the entire container.
  3. The IconButton is positioned at the top-right corner of the image, serving as the favorite action.
  4. The icon displayed depends on the _isFavorited status. A filled heart represents a favorite, while an outlined heart indicates otherwise.
  5. Tapping the favorite button toggles the _isFavorited state, effectively switching between the two heart icons. This is done via a call to setState().

Save the change to trigger a hot reload, and on the Restaurant card, see the heart button. Toggle the heart button on and off when you tap it, as shown below:

Examining the Widget Tree

Now that you’ve turned RestaurantLandscapeCard 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:

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

RestaurantLandscape Card 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.

RestaurantLandscape Card Widget Tree Element Tree Stateful Element Stateless Element State (_isFavorited = true) Tap to favorite 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 adopting 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 & State”, which covers state management and the Riverpod package — a framework built on top of InheritedWidget.

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.
© 2026 Kodeco Inc.