In the previous introduction, we know that almost all objects in Flutter are a Widget. Different from the "control" in native development, the concept of Widget in Flutter is broader. It can not only represent UI elements, but also some functional components such as: GestureDetector
widgets for gesture detection and APP theme data transfer And Theme
so on, and the controls in native development usually just refer to UI elements. In the following content, we may use concepts such as "controls" and "components" when describing UI elements. Readers need to know that they are widgets, which are just different expressions in different scenarios. Since Flutter is mainly used to build user interfaces, most of the time, readers can think of widgets as a control and don't have to worry about concepts.
In Flutter, the function of Widget is to "describe the configuration data of a UI element". It means that Widget does not actually represent the display element that is finally drawn on the device screen, but it only describes the configuration data of the display element.
In fact, the class that really represents the elements displayed on the screen in Flutter is Element
, that is to say, Widget is only Element
the configuration data described ! About the Element
details, we'll dive into the Advanced section later in this book, now, readers need to know: Widget is just a UI element configuration data, and a Widget can correspond to multipleElement
. This is because the same Widget object can be added to different parts of the UI tree, and each Element
node of the UI tree corresponds to a Widget object when it is actually rendered . in conclusion:
- Widget is actually
Element
configuration data. The Widget tree is actually a configuration tree, and the real UI rendering tree isElement
composed; however, because itElement
is generated by Widget, there is a corresponding relationship between them. In most scenarios, we The Widget tree can be broadly considered to refer to the UI control tree or the UI rendering tree. - One Widget object can correspond to multiple
Element
objects. This is easy to understand. According to the same configuration (Widget), multiple instances (Element) can be created.
Readers should keep these two points in mind.
Let's first look at the declaration of the Widget class:
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget
The class inherits fromDiagnosticableTree
,DiagnosticableTree
the "diagnostic tree", whose main function is to provide debugging information.Key
: Thiskey
attribute is similar to that in React/Vue. Itskey
main function is to decide whetherbuild
to reuse the old widget next time . The condition of the decision is in thecanUpdate()
method.createElement()
: As mentioned above, "One Widget can correspond to multipleElement
"; when Flutter Framework builds the UI tree, it will first call this method to generate theElement
object corresponding to the node . This method is implicitly called by the Flutter Framework and will not be called during our development process.debugFillProperties(...)
The method of copying the parent class is mainly to set some characteristics of the diagnostic tree.canUpdate(...)
It is a static method, which is mainly used tobuild
reuse the old widget when the Widget tree is renewed . In fact, it should be specifically: whether to use the new Widget object to updateElement
the configuration of the corresponding object on the old UI tree ; through its source code we can see, as long asnewWidget
witholdWidget
theruntimeType
andkey
will use simultaneously equalnewWidget
to update theElement
configuration objects, otherwise it will create a newElement
.
The details about the reuse of Key and Widget will be discussed in depth in the advanced part of the book. Readers only need to know that adding a key to the Widget explicitly may (but not necessarily) make the UI more efficient when it is rebuilt. Readers You can ignore this parameter for now. In the examples later in this book, the Key will only be explicitly specified when building the list item UI.
In addition, the Widget
class itself is an abstract class, the core of which is to define an createElement()
interface. In Flutter development, we generally do not directly inherit the Widget
class to implement a new component. On the contrary, we usually implement it through inheritance StatelessWidget
or StatefulWidget
indirect inheritance Widget
. Both StatelessWidget
and StatefulWidget
are directly inherited from Widget
classes, and these two classes are also two very important abstract classes in Flutter. They introduce two Widget models. Next, we will focus on these two classes.
In the previous section, we have introduced a simple StatelessWidget
, StatelessWidget
relatively simple, it inherits from Widget
class, override the createElement()
method:
@override
StatelessElement createElement() => new StatelessElement(this);
StatelessElement
Indirectly inherited from the Element
class, and StatelessWidget
corresponding (as its configuration data).
StatelessWidget
Used in scenarios that do not need to maintain state, it usually build
constructs the UI by nesting other Widgets in the method, and recursively constructs its nested Widgets during the construction process. Let's look at a simple example:
class Echo extends StatelessWidget {
const Echo({
Key key,
@required this.text,
this.backgroundColor:Colors.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 a Echo
widget that echoes a string .
By convention,
widget
the constructor parameters should use named parameters, and the necessary parameters in the named parameters should be@required
marked, which is conducive to the static code analyzer to check. In addition, when inheritingwidget
, the first parameter should usually beKey
. In addition, if the Widget needs to receive a child Widget, thechild
orchildren
parameter should usually be placed at the end of the parameter list. Also in accordance with convention, the properties of Widget should be declared as much as possible tofinal
prevent accidental changes.
Then we can use it as follows:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
The effect after running is shown in Figure 3-1:
build
The method has a context
parameter, which is BuildContext
an instance of the class, which represents the context of the current widget in the widget tree. Each widget corresponds to a context object (because each widget is a node on the widget tree). In fact, it context
is a handle for performing "related operations" on the position of the current widget in the widget tree. For example, it provides methods for traversing the widget tree from the current widget upwards and finding the parent widget according to the widget type. The following is an example of getting the parent widget in the subtree:
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context测试"),
),
body: Container(
child: Builder(builder: (context) {
// 在Widget树中向上查找最近的父级`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此处实际上是Text("Context测试")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
The effect after running is shown in Figure 3-1-1:
Note : For
BuildContext
readers, you can first understand it. As the content of this book expands, some methods of Context will be used. Readers can have an intuitive understanding of it through specific scenarios. ForBuildContext
more content, we will also introduce in-depth later in the advanced part.
It is the StatelessWidget
same, StatefulWidget
but also inherits from the Widget
class and overwrites the createElement()
method. The difference is that the returned Element
objects are not the same; in addition StatefulWidget
, a new interface is added to the class createState()
.
Below we look at StatefulWidget
the class definition:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
-
StatefulElement
Indirectly inherited from theElement
class, corresponding to StatefulWidget (as its configuration data).StatefulElement
It may be called multiple timescreateState()
to create a State object. -
createState()
Used to create the state related to the Stateful widget, which may be called multiple times during the life cycle of the Stateful widget. For example, when a Stateful widget is inserted into multiple positions of the widget tree at the same time, the Flutter framework will call this method to generate an independent State instance for each position. In fact, it is essentially aStatefulElement
corresponding State instance.
The concept of "tree" often appears in this book. It may mean different meanings in different scenarios. When talking about "widget tree", it can refer to the widget structure tree, but because there is a corresponding relationship between widget and Element (one to many ), in some scenarios (in Flutter's SDK documentation) it also means "UI tree". In the stateful widget, the State object also
StatefulElement
has a corresponding relationship (one-to-one), so in the Flutter SDK documentation, you can often see "remove the State object from the tree" or "insert the State object into the tree". description of. In fact, no matter what kind of description, its meaning is to describe "a tree of node elements that constitute a user interface". Readers do not have to worry about these concepts. The various "trees" that appear in the book, if not specifically stated, readers can abstractly think of it as "a tree of node elements that constitute the user interface".
A StatefulWidget class corresponds to a State class. State represents the state to be maintained by the corresponding StatefulWidget. The state information saved in the State can be:
- It can be read synchronously when the widget is built.
- It can be changed during the widget life cycle. When the State is changed, you can manually call its
setState()
method to notify the Flutter framework that the state has changed. After receiving the message, the Flutter framework will re-call itsbuild
method to rebuild the widget tree to update the UI. purpose.
There are two common attributes in State:
-
widget
, Which represents the widget instance associated with the State instance, which is dynamically set by the Flutter framework. Note that this association is not permanent, because in 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 rebuilding, if the widget is modified, the Flutter framework will dynamically set State.widget as a new widget instance. -
context
. The BuildContext corresponding to StatefulWidget is the same as the BuildContext of StatelessWidget.
Understanding the life cycle of State is very important for flutter development. In order to deepen the reader's impression, in this section we use an example to demonstrate the life cycle of State. In the next example, we implement a counter widget, click on it to increase the counter by 1. Since we want to save the value state of the counter, we should inherit StatefulWidget, the code is as follows:
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
_CounterWidgetState createState() => new _CounterWidgetState();
}
CounterWidget
Receive an initValue
integer parameter, which represents the initial value of the counter. Let's take a look at the State code:
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化状态
_counter=widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//点击后计数器自增
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
@override
void deactivate() {
super.deactivate();
print("deactive");
}
@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, in the new route, we only display one CounterWidget
:
Widget build(BuildContext context) {
return CounterWidget();
}
We run the application and open the routing page. After the new routing page is opened, a number 0 will appear in the center of the screen, and the console log output:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
As you can see, the first initState
method will be called when the StatefulWidget is inserted into the Widget tree .
Then we click the ⚡️ button to reload, the console output log is as follows:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
It can be seen that both initState
and didChangeDependencies
are not called at this time, but didUpdateWidget
are called at this time .
Next, we remove from the widget tree CounterWidget
and change the routing build
method to:
Widget build(BuildContext context) {
//移除计数器
//return CounterWidget();
//随便返回一个Text()
return Text("xxx");
}
Then hot reload, the log is as follows:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
We can see that when CounterWidget
removed from the widget tree, deactive
and dispose
will be called in turn.
Let's take a look at each callback function:
initState
: 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. Therefore, some one-time operations are usually performed in this callback, such as state initialization and subscription. Tree event notification, etc. Cannot be called in this callbackBuildContext.dependOnInheritedWidgetOfExactType
(this method is used to get the closest parent to the current widget on the Widget treeInheritFromWidget
,InheritedWidget
we will introduce it in a later chapter), because after the initialization is completed, the Widget treeInheritFromWidget
may also change , So the correct approach should be to call it in thebuild()
method ordidChangeDependencies()
.didChangeDependencies()
: When the object changes dependent State is invoked; for example: beforebuild()
containing oneInheritedWidget
, then afterbuild()
theInheritedWidget
change, then the timeInheritedWidget
of the child widgetdidChangeDependencies()
callback will be called. A typical scenario is that when the system language Locale or application theme changes, the Flutter framework will notify the widget to call this callback.build()
: Readers of this callback should be quite familiar by now. It is mainly used to build Widget subtrees and will be called in the following scenarios:
- After the call
initState()
. - After the call
didUpdateWidget()
. - After the call
setState()
. - After the call
didChangeDependencies()
. - After the State object is removed from one position in the tree (deactivate is called), it is reinserted into another position in the tree.
reassemble()
: This callback is specially provided for development and debugging, and will be called during hot reload. This callback will never be called in Release mode.didUpdateWidget()
: When the widget is rebuilt, the Flutter framework will callWidget.canUpdate
to detect the new and old nodes in the same position in the Widget tree, and then determine whether it needs to be updated, and call this callback if itWidget.canUpdate
returnstrue
. As mentioned before,Widget.canUpdate
it will return true when the key and runtimeType of the old and new widgets are equal at the same time, which means itdidUpdateWidget()
will be called when the key and runtimeType of the old and new widgets are equal at the same time .deactivate()
: When the State object is removed from the tree, this callback is called. In some scenarios, the Flutter framework will reinsert the State object into the tree, such as when the subtree containing the State object moves from one position of the tree to another position (which can be implemented through GlobalKey). If it is not reinserted into the tree after removal, thedispose()
method will be called immediately .dispose()
: Called when the State object is permanently removed from the tree; resources are usually released in this callback.
The life cycle of StatefulWidget is shown in Figure 3-2:
Note : When inheriting and
StatefulWidget
rewriting its method, for@mustCallSuper
the parent class method that contains annotations, the parent class method must be called first in the subclass method.
Now, we answer the question raised before, why is the build()
method placed in State (and not StatefulWidget
)? This is mainly to improve the flexibility of development. If you put the build()
method in StatefulWidget
, there will be two problems:
- State access is inconvenient.
Imagine if we StatefulWidget
have a lot of states, and each state change has to call the build
method, because the state is stored in the State, if the build
method is StatefulWidget
in, then the build
method and state are in two classes, then read during construction The state will be very inconvenient! Imagine if you really put the build
method in the StatefulWidget, since the process of building the user interface needs to rely on State, the build
method will have to add a State
parameter, which is probably as follows:
Widget build(BuildContext context, State state){
//state.counter
...
}
In this case, all the states of State can only be declared as public states, so that the state can be accessed outside the State class! However, after setting the status to public, the status will no longer be private, which will cause the modification of the status to become uncontrollable. But if the build()
method is placed in the State, the construction process can not only directly access the state, but also does not need to disclose the private state, which is very convenient.
StatefulWidget
Inconvenient inheritance .
For example, there is a base class for animation widget in Flutter AnimatedWidget
, which inherits from the StatefulWidget
class. AnimatedWidget
An abstract method is introduced in build(BuildContext context)
, and all AnimatedWidget
the animation widgets inherited from it must implement this build
method. Now imagine that if StatefulWidget
there is already a build
method in the class , as mentioned above, the build
method needs to receive a state object, which means that it AnimatedWidget
must provide its own State object (denoted as _animatedWidgetState) to its subclasses, because The subclass needs build
to call the method of the parent class in its build
method, the code may be as follows:
class MyAnimationWidget extends AnimatedWidget{
@override
Widget build(BuildContext context, State state){
//由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
//所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState
//暴露给其子类
super.build(context, _animatedWidgetState)
}
}
This is obviously unreasonable, because
AnimatedWidget
The state object isAnimatedWidget
an internal implementation detail and should not be exposed to the outside.- If you want to expose the state of the parent class to the subclass, then there must be a transfer mechanism, and this set of transfer mechanism is meaningless, because the transfer of the state between the parent and the child has nothing to do with the logic of the subclass itself.
In summary, we can find that StatefulWidget
putting the build
method in the State can bring great flexibility to development.
Since the specific logic of StatefulWidget is in its State, many times, we need to get the State object corresponding to StatefulWidget to call some methods. For example Scaffold
, the state class corresponding to the component ScaffoldState
defines the method to open SnackBar (the prompt bar at the bottom of the routing page) . We have two methods to get the State object of the parent StatefulWidget in the child widget tree.
context
The object has a findAncestorStateOfType()
method, which can look up the State object corresponding to the specified type of StatefulWidget from the current node along the widget tree. The following is an example of opening SnackBar:
Scaffold(
appBar: AppBar(
title: Text("子树中获取State对象"),
),
body: Center(
child: Builder(builder: (context) {
return RaisedButton(
onPressed: () {
// 查找父级最近的Scaffold对应的ScaffoldState对象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>();
//调用ScaffoldState的showSnackBar来弹出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
},
child: Text("显示SnackBar"),
);
}),
),
);
After the above example runs, click "Show SnackBar", the effect is shown in Figure 3-1-2:
Generally speaking, if the state of StatefulWidget is private (should not be exposed to the outside), then our code should not directly obtain its State object; if the state of StatefulWidget is to be exposed (usually there are some component operations Method), we can go directly to get its State object. However context.findAncestorStateOfType
, the method of obtaining the state of StatefulWidget is universal. We cannot specify whether the state of StatefulWidget is private at the grammatical level, so there is a default convention in Flutter development: if the state of StatefulWidget is to be exposed, it should be StatefulWidget provides a of
static method to obtain its State object, and developers can directly obtain it through this method; if State does not want to be exposed, no of
method is provided . This convention can be seen everywhere in the Flutter SDK. Therefore, the above example Scaffold
also provides a of
method, we can actually call it directly:
...//省略无关代码
// 直接通过of静态方法来获取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
Flutter also has a general State
method of obtaining objects-through GlobalKey! There are two steps:
StatefulWidget
Add to the targetGlobalKey
.
//定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //设置key
...
)
GlobalKey
GetState
objects through
_globalKey.currentState.openDrawer()
GlobalKey is a mechanism provided by Flutter to reference elements in the entire APP. If a widget is set GlobalKey
, then we can globalKey.currentWidget
obtain the globalKey.currentElement
element object corresponding to the widget by obtaining the widget object, and if the current widget is StatefulWidget
, we can globalKey.currentState
obtain the state object corresponding to the widget through .
Note: The use of GlobalKey is expensive, if there are other options, you should try to avoid using it. In addition, the same GlobalKey must be unique in the entire widget tree and cannot be repeated.
Flutter provides a set of rich and powerful basic components. On top of the basic component library, Flutter provides a set of Material style (Android default visual style) and a set of Cupertino style (iOS visual style) component library. To use the basic component library, you need to first import:
import 'package:flutter/widgets.dart';
Below we introduce the commonly used components.
Text
: This component allows you to create a formatted text.Row
,Column
: These layout widgets with flexible space allow you to create flexible layouts in the horizontal (Row) and vertical (Column) directions. Its design is based on the Flexbox layout model in web development.Stack
: Substituted linear layout (dollop: and AndroidFrameLayout
like),Stack
allows the child widget stack, you can usePositioned
to locate them with respect to theStack
position of the four sides of the vertical and horizontal. Stacks is designed based on the absolute positioning (absolute positioning) layout model in Web development.Container
:Container
Allows you to create rectangular visual elements. A container can be decoratedBoxDecoration
, such as a background, a border, or a shadow.Container
It can also have margins, padding, and constraints applied to its size. In addition,Container
a matrix can be used to transform it in three-dimensional space.
Flutter provides a rich set of Material components, which can help us build applications that follow Material Design specifications. The Material application MaterialApp
starts with a component, which creates some necessary components at the root of the application, such as Theme
components, which are used to configure the theme of the application. Whether to use MaterialApp
it is completely optional, but it is a good practice to use it. In the previous examples, we have used a plurality Material components, such as: Scaffold
, AppBar
, FlatButton
and the like. To use the Material component, you need to introduce it first:
import 'package:flutter/material.dart';
Flutter also provides a rich set of Cupertino-style components. Although it is not as rich as Material components, it is still being improved. It is worth mentioning that there are some components in the Material component library that can switch the performance style according to the actual operating platform. For example MaterialPageRoute
, when switching the route, if it is the Android system, it will use the Android system default page switching animation (from bottom to top) ); If it is an iOS system, it will use the default page switching animation of the iOS system (from right to left). Since there is no example of Cupertino component in the previous example, let's implement a simple Cupertino component style page:
//导入cupertino widget库
import 'package:flutter/cupertino.dart';
class CupertinoTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Cupertino Demo"),
),
child: Center(
child: CupertinoButton(
color: CupertinoColors.activeBlue,
child: Text("Press"),
onPressed: () {}
),
),
);
}
}
The following (Figure 3-3) is a screenshot of the page effect on iPhoneX:
Examples of the later chapters of this chapter will use some layout class components, such as Scaffold
, Row
, Column
etc., these components will "Layout class components," described in detail in a later chapter, the reader is not the first concern.
Flutter provides a wealth of components. In actual development, you can use them as you want, without worrying that the introduction of too many component libraries will make your application installation package larger. This is not web development. Dart will only compile at compile time. The code you used. Since both Material and Cupertino are on top of the basic component library, if we introduce one of these two in our application, we don't need to introduce it again flutter/widgets.dart
because they have already been introduced internally.