9.1 The Concept of Widget
From the previous introduction, we know that in Flutter, almost all objects are widgets. Unlike the concept of "controls" in native development, widgets in Flutter have a broader meaning. They can represent not only UI elements but also functional components, such as GestureDetector for gesture detection and Theme for passing app theme data. In contrast, controls in native development usually refer only to UI elements. In the following content, when we refer to UI elements, we might use terms like "controls" or "components," and readers should understand that these are all widgets—just different expressions in different contexts. Since Flutter is primarily used for building user interfaces, readers can generally consider a widget as a control without getting bogged down in terminology.
In Flutter, the UI and event handling are constructed by nesting widgets, so remember: everything in Flutter is a widget.
9.2 Widget Interface
In Flutter, the function of a widget is to "describe the configuration information of a UI element." This means that a widget does not represent the final display element drawn on the device's screen. The so-called configuration information refers to the parameters received by the widget. For example, for a Text widget, the content of the text, alignment, and text style are all part of its configuration information. Let's first look at the declaration of the Widget class:
@immutable // Immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
@override
String toStringShort() {
final String type = objectRuntimeType(this, 'Widget');
return key == null ? type : '$type-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
@override
@nonVirtual
bool operator ==(Object other) => super == other;
@override
@nonVirtual
int get hashCode => super.hashCode;
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
...
}The @immutable annotation indicates that the widget is immutable, which means that the properties (i.e., configuration information) defined in the widget must be immutable (final). Why are property changes not allowed in a widget? Because if properties change in Flutter, it will rebuild the widget tree, i.e., create a new widget instance to replace the old one. Therefore, allowing property changes in a widget is meaningless because once the widget's properties change, it will be replaced. This is why properties defined in the widget must be final.
The widget class inherits from DiagnosticableTree, which serves as a "diagnostic tree" primarily to provide debugging information.
Key: This
keyproperty is similar to thekeyin React/Vue, and its main role is to determine whether to reuse the old widget during the next build. The conditions for this are defined in thecanUpdate()method.createElement(): As mentioned earlier, "a widget can correspond to multiple Elements." When building the UI tree, the Flutter framework first calls this method to generate the corresponding
Elementobject for the node. This method is called implicitly by the Flutter framework and is generally not called directly during development.debugFillProperties(...): This overrides the parent class method and is mainly used to set some features of the diagnostic tree.
canUpdate(...): This is a static method primarily used to reuse the old widget when the widget tree is rebuilt. Specifically, it checks whether to update the configuration of the corresponding
Elementobject on the old UI tree with the new widget object. As seen in its source code, it will reuse the new widget to update theElementobject’s configuration if theruntimeTypeandkeyofnewWidgetandoldWidgetare equal; otherwise, it will create a newElement.
Details regarding Key and widget reuse will be discussed in more advanced sections later in the book. For now, readers should know that explicitly adding a key to a widget might (but not necessarily) make the UI more efficient during reconstruction. This parameter can be ignored for now, as it will be explained in detail later in the book.
Additionally, the Widget class itself is an abstract class, with the most crucial part being the definition of the createElement() interface. In Flutter development, we generally do not directly inherit the Widget class to implement a new component. Instead, we typically inherit from StatelessWidget or StatefulWidget to indirectly inherit from the Widget class. Both StatelessWidget and StatefulWidget are direct subclasses of Widget, and these two classes are very important abstract classes in Flutter. They introduce two widget models, which we will focus on introducing next.
9.3 The Four Trees in Flutter
Since a widget merely describes the configuration information of a UI element, who is responsible for the actual layout and rendering? The processing flow of the Flutter framework is as follows:
Generate an
Elementtree from theWidgettree, with nodes in theElementtree inheriting from theElementclass.Generate a
Rendertree (rendering tree) from theElementtree, with nodes in the rendering tree inheriting from theRenderObjectclass.Generate a
Layertree from the rendering tree, which is then displayed on the screen, with nodes in the Layer tree inheriting from theLayerclass.
The actual layout and rendering logic occur in the Render tree. The Element acts as the glue between Widget and RenderObject, functioning as an intermediary proxy. Let’s illustrate this with an example, assuming we have the following widget tree:
Container( // A container widget
color: Colors.blue, // Set the background color of the container
child: Row( // Align child widgets horizontally
children: [
Image.network('https://www.example.com/1.png'), // Widget to display an image
const Text('A'),
],
),
);Note that if the Container sets a background color, it will create a new ColoredBox to fill the background, with the relevant logic as follows:
if (color != null) current = ColoredBox(color: color!, child: current);
The Image widget will render the image via RawImage, while the Text widget will render the text using RichText. Thus, the final structure of the widget tree, element tree, and rendering tree is shown in Figure:

Here, it is important to note:
In the three trees,
WidgetandElementcorrespond one-to-one, but they do not correspond one-to-one withRenderObject. For example, bothStatelessWidgetandStatefulWidgetdo not have correspondingRenderObject.Before displaying on the screen, the rendering tree will generate a
Layertree, which will be discussed in detail in later chapters. For now, readers should just remember the three trees mentioned above.
9.4 StatelessWidget
1. Introduction
In the previous chapters, we briefly introduced StatelessWidget. StatelessWidget is relatively straightforward; it inherits from the Widget class and overrides the createElement() method:
@override StatelessElement createElement() => StatelessElement(this);
StatelessElement indirectly inherits from the Element class and corresponds to StatelessWidget (as its configuration data).
StatelessWidget is used in scenarios where no state needs to be maintained. It typically constructs the UI in the build method by nesting other widgets, recursively building its nested widgets in the process. Let’s look at a simple example:
class Echo extends StatelessWidget {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, // Default is grey
}):super(key:key);
final String text;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}The above code implements an Echo widget that echoes a string.
By convention, the parameters of a widget's constructor should use named parameters, and required parameters should be marked with the required keyword, which helps static code analyzers with checks. When inheriting from a widget, the first parameter is typically Key. Additionally, if a widget needs to accept child widgets, the child or children parameters are usually placed at the end of the parameter list. Following convention, widget properties should be declared as final whenever possible to prevent accidental changes.
We can use it as follows:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}The result after running looks like Figure :

2. Context
The build method has a context parameter, which is an instance of the BuildContext class. It represents the context of the current widget within the widget tree. Each widget corresponds to a context object (because each widget is a node in the widget tree). In fact, context serves as a handle for performing "related operations" based on the position of the current widget in the widget tree. For instance, it provides methods for traversing up the widget tree from the current widget and for locating parent widgets by type. Here’s an example of retrieving a parent widget in a subtree:
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context Test"),
),
body: Container(
child: Builder(builder: (context) {
// Look up the nearest parent `Scaffold` widget in the widget tree
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// Directly return the title of AppBar, which is actually Text("Context Test")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}The result after running looks like Figure :

Note: Readers can familiarize themselves with BuildContext for now. As the content of this book unfolds, methods related to Context will be utilized, allowing readers to develop an intuitive understanding through specific scenarios. More information regarding BuildContext will also be covered in greater detail in the advanced sections later in the book.
9.5 StatefulWidget
Similar to StatelessWidget, StatefulWidget also inherits from the Widget class and overrides the createElement() method. However, the returned Element object is different; additionally, the StatefulWidget class introduces a new interface, createState().
Let’s examine the class definition of StatefulWidget:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key? key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}StatefulElement indirectly inherits from the Element class and corresponds to StatefulWidget (as its configuration data). The StatefulElement may call createState() multiple times to create a state (State) object.
createState() is used to create the state associated with the StatefulWidget, and it may be called multiple times during the lifecycle of the StatefulWidget. For example, when a StatefulWidget is inserted into multiple locations in the widget tree, the Flutter framework will call this method to generate an independent State instance for each location. Essentially, a StatefulElement corresponds to a single State instance.
In a StatefulWidget, there is a one-to-one relationship between the State object and StatefulElement. Therefore, in the Flutter SDK documentation, you may often see descriptions like "removing a State object from the tree" or "inserting a State object into the tree," where "the tree" refers to the Element tree generated from the widget tree. The SDK documentation frequently mentions "trees," and we can determine which tree is being referred to based on the context. Regardless of which tree it is, the ultimate goal is to describe the structure and rendering information of the UI. Thus, when encountering the concept of "trees" in Flutter, unless specified otherwise, we can understand it as "a node tree that constitutes the user interface." Readers need not get bogged down in these concepts; the key takeaway is to "grasp the essence and forget the form."
9.6 State
1. Introduction
A StatefulWidget class corresponds to a State class. State represents the state that the corresponding StatefulWidget needs to maintain. The state information stored in State can:
Be synchronously read when the widget is built.
Be modified during the widget's lifecycle. When the state is changed, the
setState()method can be called manually to notify the Flutter framework of the change. After receiving the message, Flutter will call itsbuildmethod again to reconstruct the widget tree, thereby updating the UI.
There are two commonly used properties in State:
widget: This represents the widget instance associated with theStateinstance, dynamically set by the Flutter framework. Note that this association is not permanent because, during the application's lifecycle, the widget instance at a specific node on the UI tree may change during reconstruction. However, theStateinstance is only created the first time it is inserted into the tree. When the widget is rebuilt, if the widget has changed, Flutter dynamically setsState.widgetto the new widget instance.context: TheBuildContextcorresponding to theStatefulWidget, serving the same role asBuildContextin aStatelessWidget.
2. State Lifecycle
Understanding the lifecycle of State is crucial for Flutter development. To make it more memorable, we will demonstrate the lifecycle of State through an example. In the following example, we will continue using the counter functionality to implement a CounterWidget component. When clicked, the counter increments by 1. Since we need to store the counter's value as state, we should extend StatefulWidget. The code is as follows:
class CounterWidget extends StatefulWidget {
const CounterWidget({Key? key, this.initValue = 0});
final int initValue;
@override
_CounterWidgetState createState() => _CounterWidgetState();
}CounterWidget receives an initValue integer parameter, which represents the initial value of the counter. Now, let’s look at the State code:
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
@override
void initState() {
super.initState();
// Initialize state
_counter = widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: TextButton(
child: Text('$_counter'),
// Increment counter on click
onPressed: () => setState(
() => ++_counter,
),
),
),
);
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget ");
}
@override
void deactivate() {
super.deactivate();
print("deactivate");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}Next, we create a new route, where we only display the CounterWidget:
class StateLifecycleTest extends StatelessWidget {
const StateLifecycleTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CounterWidget();
}
}When we run the application and open the new route, a 0 appears in the center of the screen, and the console log outputs:
I/flutter (5436): initState I/flutter (5436): didChangeDependencies I/flutter (5436): build
As you can see, when a StatefulWidget is inserted into the widget tree, the initState method is called first.
Then, we click the ⚡️ button for a hot reload, and the console outputs:
I/flutter (5436): reassemble I/flutter (5436): didUpdateWidget I/flutter (5436): build
At this point, neither initState nor didChangeDependencies are called, but didUpdateWidget is.
Next, we remove the CounterWidget from the widget tree by modifying the build method of StateLifecycleTest:
Widget build(BuildContext context) {
// Remove the counter
// return CounterWidget();
// Return a random Text() instead
return Text("xxx");
}After a hot reload, the log is as follows:
I/flutter (5436): reassemble I/flutter (5436): deactivate I/flutter (5436): dispose
We can see that when the CounterWidget is removed from the widget tree, deactivate and dispose are called in sequence.
Let’s now go over the various callback functions:
initState: Called when the widget is first inserted into the widget tree. For each
Stateobject, the Flutter framework will call this callback only once, so it is usually used for one-time operations, such as state initialization or subscribing to subtree event notifications. You should not callBuildContext.dependOnInheritedWidgetOfExactType()in this callback because, after initialization, theInheritedWidgetin the widget tree might change. The correct practice is to call it inbuild()ordidChangeDependencies().didChangeDependencies: Called when the dependencies of the
Stateobject change. For example, if the previousbuild()method included anInheritedWidget(introduced in Chapter 7) and theInheritedWidgetchanges in a subsequentbuild(), this callback will be called. A typical scenario is when the system locale or app theme changes, and Flutter notifies the widget to call this callback. It is important to note thatdidChangeDependenciesis called when the component is first created and mounted (including when it is recreated).build(): You should already be quite familiar with this callback. It is primarily used to construct the widget subtree and is called in the following situations:
After calling
initState().After calling
didUpdateWidget().After calling
setState().After calling
didChangeDependencies().After the
Stateobject is removed from one location in the tree (callingdeactivate) and inserted into another location.reassemble(): This callback is provided specifically for development and debugging. It is called during a hot reload and is never called in release mode.
didUpdateWidget(): When the widget is rebuilt, the Flutter framework calls
widget.canUpdateto check the new and old nodes at the same position in the widget tree and decide whether to update. Ifwidget.canUpdatereturnstrue, this callback is invoked. As mentioned earlier,widget.canUpdatereturnstruewhen thekeyandruntimeTypeof the new and old widgets are equal. Thus, when thekeyandruntimeTypeof the new and old widgets are equal,didUpdateWidget()will be called.deactivate(): Called when the
Stateobject is removed from the tree. In some scenarios, the Flutter framework may reinsert theStateobject into the tree, such as when a subtree containing thisStateobject is moved to another location in the tree (this can be achieved usingGlobalKey). If it is not reinserted, thedispose()method will be called afterward.dispose(): Called when the
Stateobject is permanently removed from the tree, typically used to release resources.
The lifecycle of a StatefulWidget is illustrated in Figure:

Note: When overriding methods in a StatefulWidget, for methods marked with @mustCallSuper in the parent class, you must call the parent class's method within the subclass method.
9.7 Accessing the State Object in the Widget Tree
Since the specific logic of a StatefulWidget is encapsulated within its State, there are many occasions when we need to access the State object corresponding to a StatefulWidget to call certain methods. For example, the state class ScaffoldState associated with the Scaffold component defines a method to open a SnackBar (the bottom prompt bar on the route page). We have two methods to retrieve the State object of the parent StatefulWidget in the child widget tree.
1. Accessing via Context
The context object has a method findAncestorStateOfType(), which can search upwards in the widget tree from the current node to find the State object corresponding to the specified type of StatefulWidget. Below is an example of how to implement opening a SnackBar:
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
@override
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Accessing State Object in Child Tree"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// Find the nearest Scaffold corresponding to the ScaffoldState object
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// Open the drawer menu
_state.openDrawer();
},
child: Text('Open Drawer Menu 1'),
);
}),
],
),
),
drawer: Drawer(),
);
}
}Generally, if the state of a StatefulWidget is private (should not be exposed to the outside), then we should not directly access its State object in our code. If the state of a StatefulWidget is intended to be exposed (usually with some operational methods), we can directly access its State object. However, the method to obtain the state of a StatefulWidget via context.findAncestorStateOfType is universal, and we cannot specify the privacy of a StatefulWidget's state at the syntax level. Therefore, in Flutter development, there is a default convention: if the state of a StatefulWidget is meant to be exposed, a static of method should be provided in the StatefulWidget to retrieve its State object, allowing developers to access it directly through this method. If the State should not be exposed, the of method is not provided. This convention is commonly seen throughout the Flutter SDK. Thus, the Scaffold in the example above also provides an of method, which we can actually call directly:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// Directly obtain the ScaffoldState through the of static method
ScaffoldState _state = Scaffold.of(context);
// Open the drawer menu
_state.openDrawer();
},
child: Text('Open Drawer Menu 2'),
);
}),Similarly, if we want to display a SnackBar, we can call it using the following code:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("I am a SnackBar")),
);
},
child: Text('Show SnackBar'),
);
}),After running the above example, clicking "Show SnackBar" will yield the result shown in Figure:

2. Accessing via GlobalKey
Flutter provides another general method for accessing the State object—through a GlobalKey. The process involves two steps:
Add a
GlobalKeyto the targetStatefulWidget.
// Define a globalKey. Since GlobalKey needs to maintain global uniqueness, we use a static variable to store it. static GlobalKey<ScaffoldState> _globalKey = GlobalKey(); ... Scaffold( key: _globalKey, // Set the key ... )
Use the
GlobalKeyto access theStateobject.
_globalKey.currentState.openDrawer();
A GlobalKey is a mechanism provided by Flutter for referencing elements throughout the entire app. If a widget is assigned a GlobalKey, we can obtain that widget object via globalKey.currentWidget, the corresponding element object via globalKey.currentElement, and if the current widget is a StatefulWidget, we can access its corresponding state object via globalKey.currentState.
Note: Using GlobalKey incurs significant overhead; if there are other options available, it should be avoided whenever possible. Additionally, the same GlobalKey must be unique within the entire widget tree and cannot be duplicated.
9.8 Creating Custom Widgets via RenderObject
StatelessWidget and StatefulWidget are used to compose other components, but they do not have their own corresponding RenderObject. Many foundational components in Flutter's component library, such as Text, Column, Align, are not implemented through StatelessWidget or StatefulWidget. It's like building blocks—StatelessWidget and StatefulWidget can arrange the blocks in different ways, but first, we need blocks, which are realized through custom RenderObject. In fact, the most primitive way to define components in Flutter is by defining a RenderObject, while StatelessWidget and StatefulWidget are just helper classes. Let’s demonstrate how to define a component via RenderObject:
class CustomWidget extends LeafRenderObjectWidget {
@override
RenderObject createRenderObject(BuildContext context) {
// Create RenderObject
return RenderCustomObject();
}
@override
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// Update RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox {
@override
void performLayout() {
// Implement layout logic
}
@override
void paint(PaintingContext context, Offset offset) {
// Implement painting
}
}If a component does not contain child components, we can directly inherit from LeafRenderObjectWidget, which is a subclass of RenderObjectWidget. RenderObjectWidget itself inherits from Widget. Let’s take a look at its implementation:
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
const LeafRenderObjectWidget({Key? key}) : super(key: key);
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}It's straightforward: it implements the createElement method, which creates an Element object of type LeafRenderObjectElement for the component. If the custom widget can contain child components, you can choose to inherit from SingleChildRenderObjectWidget or MultiChildRenderObjectWidget, depending on the number of children. They also implement the createElement() method, returning different types of Element objects.
Next, we override the createRenderObject method, which is defined in RenderObjectWidget. This method is called by the component's corresponding Element (when constructing the render tree) to generate the render object. Our main task is to implement the render object class returned by createRenderObject. In this case, it's RenderCustomObject. The updateRenderObject method is used to update the component's render object when the component tree's state changes without needing to recreate the RenderObject.
The RenderCustomObject class inherits from RenderBox, and RenderBox inherits from RenderObject. In RenderCustomObject, we need to implement logic for layout, painting, and event handling. The knowledge needed to implement these will be covered throughout this book, so don't worry; we will introduce it step by step in later chapters.
9.9 Overview of the Flutter SDK's Built-in Component Libraries
Flutter provides a rich and powerful set of foundational components. On top of the basic component library, Flutter also offers a Material-style component library (Android’s default visual style) and a Cupertino-style library (iOS visual style). To use the basic component library, you need to first import:
import 'package:flutter/widgets.dart';
Let’s introduce some commonly used components.
1. Basic Components
Text: This component allows you to create formatted text.
Row, Column: These are layout widgets with flexible spaces, allowing you to create flexible layouts in horizontal (
Row) and vertical (Column) directions. They are designed based on the Flexbox layout model used in web development.Stack: This replaces linear layouts (similar to
FrameLayoutin Android). Stack allows child widgets to overlap, and you can position them relative to the four edges of theStackusing thePositionedwidget. Stacks are designed based on the absolute positioning layout model used in web development.Container:
Containerallows you to create rectangular visual elements. AContainercan be decorated with aBoxDecoration, such as a background, a border, or a shadow.Containercan also have margins, padding, and constraints applied to its size. Additionally,Containercan be transformed in 3D space using matrices.
2. Material Components
Flutter provides a rich set of Material components that help you build applications that follow the Material Design guidelines. Material applications start with the MaterialApp component, which creates several essential components at the root of the application, such as the Theme component, which configures the app's theme. Using MaterialApp is optional, but it's a good practice. In previous examples, we have already used several Material components, such as Scaffold, AppBar, and TextButton. To use Material components, you need to first import:
import 'package:flutter/material.dart';
3. Cupertino Components
Flutter also provides a set of Cupertino-style components. Although not as extensive as the Material components, they are continually being improved. It’s worth noting that some components in the Material library can switch their visual styles based on the platform. For example, MaterialPageRoute uses Android’s default page transition animation (from bottom to top) on Android and iOS’s default transition animation (from right to left) on iOS. Since we haven't shown examples of Cupertino components in the previous sections, let's implement a simple Cupertino-style page:
// Import the cupertino widget library
import 'package:flutter/cupertino.dart';
class CupertinoTestRoute extends StatelessWidget {
const CupertinoTestRoute({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text("Cupertino Demo"),
),
child: Center(
child: CupertinoButton(
color: CupertinoColors.activeBlue,
child: const Text("Press"),
onPressed: () {},
),
),
);
}
}Below (Figure) is a screenshot of the page on an iPhone X:

9.10 Summary
Flutter's widget types are divided into StatefulWidget and StatelessWidget. Readers need to deeply understand the differences between them, as widgets will be the cornerstone of building Flutter applications.
Flutter provides a rich set of components, and in actual development, we can use them as needed without worrying about introducing too many component libraries that would make your app package too large. This is not web development—Dart only compiles the code you use during the build process. Since Material and Cupertino are both built on top of the basic component library, if you include one of them in your application, there's no need to import flutter/widgets.dart again, as it’s already included internally.
Additionally, note that the examples in the later sections of this chapter will use some layout components, such as Scaffold, Row, and Column. These components will be covered in detail in the "Layout Components" chapter later on, so there's no need to focus on them just yet.