A First Look at Flutter (1): Widgets

1. Widget

1.1 canUpdate method

canUpdate(...) is a static method used primarily to reuse existing widgets when the widget tree is rebuilt.

  • The basic function: whether to use the new widget object to update the configuration of the corresponding Element object in the old UI tree.
  • Judgment rule: As long as newWidget and oldWidget’s runtimeType and key are equal at the same time, new widget updates the configuration of the Element object, otherwise a new Element will be created.

1.2 Four trees in Flutter

1.2.1 Basic Contacts and Responsibilities

  1. Generates an Element tree based on the Widget tree. The nodes in the Element tree all inherit from the Element class.
  2. Generates a Render tree (rendering tree) based on the Element tree. The nodes in the rendering tree all inherit from the RenderObject class.
  3. Generates a Layer tree based on the rendering tree, and then displays it on the screen. The nodes in the Layer tree all inherit from the Layer class.
  • Layout and rendering logic is in the Render tree.
  • Element is the glue between Widget and RenderObject.

1.2.2 Example

1
2
3
4
5
6
7
8
9
Container( // A container widget
color: Colors.blue, // Set container background color
child: Row( // can arrange the children widget in the horizontal direction
children: [
Image.network('https://www.example.com/1.png'), // Showing pictures of widget
const Text('A'),
],
),
);

Note that if the Container sets a background color, a new ColoredBox will be created inside the Container to fill the background. The relevant logic is as follows:

1
2
if (color != null)
current = ColoredBox(color: color!, child: current);
  • Image internally uses RawImage to render images
  • Text will render text internally through RichText

1.2.3 The relationship between four trees

  1. Widget and Element have a one-to-one correspondence.
  2. Widget does not correspond to RenderObject one-to-one. For example, StatelessWidget and StatefulWidget have no corresponding RenderObject.
  3. The rendering tree will generate a Layer tree before being displayed on the screen.

1.3 Widget constructor

According to convention, the widget's constructor parameters should use named parameters, and the required keyword should be added to the parameters that must be passed in the named parameters. This is beneficial to the static code analyzer for inspection; when inheriting a widget, the first parameter should usually be Key. In addition, if the widget needs to receive child widgets, the child or children parameters should usually be placed at the end of the parameter list. Also according to convention, widget properties should be declared as final as much as possible to prevent accidental changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Echo extends StatelessWidget  {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //Default is gray
}):super(key:key);

final String text;
final Color backgroundColor;

@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}

2. StatelessWidget

2.1 Basic concepts

StatelessWidget inherits from the widget class and overrides the createElement() method:

1
2
@override
StatelessElement createElement() => StatelessElement(this);

StatelessElement indirectly inherits from the Element class.

  • Function: StatelessWidget is used in scenarios that do not need to maintain state. It usually builds the UI by nesting other widgets in the build method. During the construction process, its nested widgets will be built recursively.

2.2 Context

The build method takes a context parameter, an instance of BuildContext that represents the current widget's position in the widget tree. Each widget has a corresponding context object because every widget is a node in that tree.

3. StatefulWidget

3.1 Basic concepts

Like StatelessWidget, StatefulWidget extends the Widget class and overrides createElement(). It returns a different type of Element and introduces a new method, createState().

1
2
3
4
5
6
7
8
9
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);

@override
StatefulElement createElement() => StatefulElement(this);

@protected
State createState();
}

StatefulElement may call createState() multiple times to create a state object.
Timing of multiple calls:

  • When a StatefulWidget is inserted into multiple locations in the widget tree at the same time, the Flutter framework will call this method to generate an independent State instance for each location. In fact, it is essentially a StatefulElement corresponding to a State instance.

3.2 Relationship between the three

  • When a StatefulWidget is inserted into multiple locations in the widget tree at the same time, an independent State instance is generated for each location.
  • State object and the StatefulElement.
  • State When the state of the object changes, a new widget instance may be rebuilt.

3.3 State

State:

  1. Can be read synchronously when the widget is built.
  2. Can be changed during the widget life cycle. When the State is changed, its setState() method can be manually called to notify the Flutter framework that the state has changed. After receiving the message, the Flutter framework will call its build method again to rebuild the widget tree, thereby updating the UI.

3.3.1 Two attributes

  1. widget, which represents the widget instance associated with this State instance and is dynamically set by the Flutter framework. Note that this association is not permanent, because during the application life cycle, the widget instance of a node on the UI tree may change when it is rebuilt, but the State instance will only be created the first time it is inserted into the tree. When rebuilt, if the widget is modified, the Flutter framework will dynamically set State.widget to the new widget instance.

  2. context. The BuildContext corresponding to StatefulWidget has the same function as the BuildContext of StatelessWidget.

3.3.2 Life cycle

When created:

1
2
3
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build

When hot reloading:

1
2
3
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build

Remove in widget tree, then hot reload:

1
2
3
4
5
6
 Widget build(BuildContext context) {
//Remove counter
//return CounterWidget ();
//Return any Text()
return Text("xxx");
}
1
2
3
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose

3.3.3 initState

Calling timing: It will be called when the widget is inserted into the widget tree for the first time. For each State object, the Flutter framework will only call this callback once.
Function: Usually some one-time operations are performed in this callback, such as state initialization, subtree event notification subscription, etc.

3.3.4 didChangeDependencies

Calling timing:

  • Will be called when the dependencies of the State object change.
  • The corresponding didChangeDependencies will also be called when the component is mounted for the first time (including re-creation).

Function: When the system language Locale or application theme changes, the Flutter framework will notify the widget to call this callback.

3.3.5 Build

Calling timing:

  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 (deactivate will be called) and reinserted into another location in the tree.

Function: Mainly used to build widget subtrees.

3.3.6 didUpdateWidget

Calling timing: When the widget is rebuilt, the Flutter framework will call widget.canUpdate to detect the widget. The old and new nodes at the same position in the tree, and then decide whether they need to be updated. If widget.canUpdate returns true, this callback will be called.
Function: Save performance overhead.

3.3.7 Deactivate

Call time: When the State object is removed from the tree.

3.3.8 Dispose

Calling timing: When the State object is permanently removed from the tree; if the removed subtree is not re-inserted into the tree after deactivate is called, the dispose() method will be called immediately.
Role: Resources are usually released in this callback.

3.4 StatefulWidget life cycle diagram

3.5 @mustCallSuper

When overriding methods in a class that extends StatefulWidget, every superclass method annotated with @mustCallSuper must be called from the subclass implementation.

4. Get the State object in the widget tree

4.1 The first method: obtain through Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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("Get from subtreeStateobject"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// Find the ScaffoldState object corresponding to the parent’s nearest Scaffold
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// Open drawer menu
_state.openDrawer();
},
child: Text('Open drawer menu1'),
);
}),
],
),
),
drawer: Drawer(),
);
}
}

4.2 Convention for Static of Methods

Generally speaking, if the state of StatefulWidget is private (should not be exposed to the outside), then we should not directly obtain its State object in our code; if the state of StatefulWidget is to be exposed (usually there are some component operation methods), we can directly obtain its State object. However, the method of obtaining the state of StatefulWidget through context.findAncestorStateOfType is universal. We cannot specify whether the state of StatefulWidget is private at the syntax level, so there is a default convention in Flutter development: If the state of StatefulWidget is to be exposed, an of static method should be provided in StatefulWidget to obtain its State object, and developers can obtain it directly through this method; if If State does not want to be exposed, it does not provide the of method. This convention can be found everywhere in the Flutter SDK. Therefore, the Scaffold in the above example also provides a of method, and we can actually call it directly:

1
2
3
4
5
6
7
8
9
10
11
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// Obtain ScaffoldState directly through the of static method
ScaffoldState _state = Scaffold.of(context);
// Open drawer menu
_state.openDrawer();
},
child: Text('Open drawer menu2'),
);
}),
1
2
3
4
5
6
7
8
9
10
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("I am SnackBar")),
);
},
child: Text('showingSnackBar'),
);
}),

4.3 The second method: through GlobalKey

4.3.1 Add GlobalKey to the target StatefulWidget

1
2
3
4
5
6
7
// defines a globalKey. Since GlobalKey needs to maintain global uniqueness, we use static variable storage
static GlobalKey<ScaffoldState> _globalKey = GlobalKey();
...
Scaffold(
key: _globalKey , //Settingkey
...
)

4.3.2 Obtain State object through GlobalKey

1
_globalKey.currentState.openDrawer()

GlobalKey is a mechanism provided by Flutter to reference elements throughout the App. If a widget is set with GlobalKey, then we can obtain the widget object through globalKey.currentWidget and globalKey.currentElement to obtain the element object corresponding to the widget. If the current widget If it is StatefulWidget, you can obtain the state object corresponding to the widget through globalKey.currentState.