Flutter (9): Introduction to Widgets

Time: Column:Mobile & Frontend views:265

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 key property is similar to the key in 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 the canUpdate() 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 Element object 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 Element object on the old UI tree with the new widget object. As seen in its source code, it will reuse the new widget to update the Element object’s configuration if the runtimeType and key of newWidget and oldWidget are equal; otherwise, it will create a new Element.

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:

  1. Generate an Element tree from the Widget tree, with nodes in the Element tree inheriting from the Element class.

  2. Generate a Render tree (rendering tree) from the Element tree, with nodes in the rendering tree inheriting from the RenderObject class.

  3. Generate a Layer tree from the rendering tree, which is then displayed on the screen, with nodes in the Layer tree inheriting from the Layer class.

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:

Flutter (9): Introduction to Widgets

Here, it is important to note:

  • In the three trees, Widget and Element correspond one-to-one, but they do not correspond one-to-one with RenderObject. For example, both StatelessWidget and StatefulWidget do not have corresponding RenderObject.

  • Before displaying on the screen, the rendering tree will generate a Layer tree, 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 :

Flutter (9): Introduction to Widgets

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 :

Flutter (9): Introduction to Widgets

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 its build method 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 the State instance, 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, the State instance is only created the first time it is inserted into the tree. When the widget is rebuilt, if the widget has changed, Flutter dynamically sets State.widget to the new widget instance.

  • context: The BuildContext corresponding to the StatefulWidget, serving the same role as BuildContext in a StatelessWidget.

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 State object, 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 call BuildContext.dependOnInheritedWidgetOfExactType() in this callback because, after initialization, the InheritedWidget in the widget tree might change. The correct practice is to call it in build() or didChangeDependencies().

  • didChangeDependencies: Called when the dependencies of the State object change. For example, if the previous build() method included an InheritedWidget (introduced in Chapter 7) and the InheritedWidget changes in a subsequent build(), 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 that didChangeDependencies is 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:

    1. After calling initState().

    2. After calling didUpdateWidget().

    3. After calling setState().

    4. After calling didChangeDependencies().

    5. After the State object is removed from one location in the tree (calling deactivate) 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.canUpdate to check the new and old nodes at the same position in the widget tree and decide whether to update. If widget.canUpdate returns true, this callback is invoked. As mentioned earlier, widget.canUpdate returns true when the key and runtimeType of the new and old widgets are equal. Thus, when the key and runtimeType of the new and old widgets are equal, didUpdateWidget() will be called.

  • deactivate(): Called when the State object is removed from the tree. In some scenarios, the Flutter framework may reinsert the State object into the tree, such as when a subtree containing this State object is moved to another location in the tree (this can be achieved using GlobalKey). If it is not reinserted, the dispose() method will be called afterward.

  • dispose(): Called when the State object is permanently removed from the tree, typically used to release resources.

The lifecycle of a StatefulWidget is illustrated in Figure:

Flutter (9): Introduction to Widgets

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:

Flutter (9): Introduction to Widgets

2. Accessing via GlobalKey

Flutter provides another general method for accessing the State object—through a GlobalKey. The process involves two steps:

  1. Add a GlobalKey to the target StatefulWidget.

// 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
    ...
)
  1. Use the GlobalKey to access the State object.

_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 FrameLayout in Android). Stack allows child widgets to overlap, and you can position them relative to the four edges of the Stack using the Positioned widget. Stacks are designed based on the absolute positioning layout model used in web development.

  • Container: Container allows you to create rectangular visual elements. A Container can be decorated with a BoxDecoration, such as a background, a border, or a shadow. Container can also have margins, padding, and constraints applied to its size. Additionally, Container can 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:

Flutter (9): Introduction to Widgets


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.