Skip to content

Latest commit

 

History

History
533 lines (374 loc) · 30.8 KB

chapter_3.1.md

File metadata and controls

533 lines (374 loc) · 30.8 KB

3.1 Introduction to Widget

3.1.1 Concept

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: GestureDetectorwidgets for gesture detection and APP theme data transfer And Themeso 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.

3.1.2 Widget and Element

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 Elementthe configuration data described ! About the Elementdetails, 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 Elementnode of the UI tree corresponds to a Widget object when it is actually rendered . in conclusion:

  • Widget is actually Elementconfiguration data. The Widget tree is actually a configuration tree, and the real UI rendering tree is Elementcomposed; however, because it Elementis 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 Elementobjects. 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.

3.1.3 Widget main interface

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;
 }
}
  • WidgetThe class inherits from DiagnosticableTree, DiagnosticableTreethe "diagnostic tree", whose main function is to provide debugging information.
  • Key: This keyattribute is similar to that in React/Vue. Its keymain function is to decide whether buildto reuse the old widget next time . The condition of the decision is in the canUpdate()method.
  • createElement(): As mentioned above, "One Widget can correspond to multiple Element"; when Flutter Framework builds the UI tree, it will first call this method to generate the Elementobject 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 to buildreuse the old widget when the Widget tree is renewed . In fact, it should be specifically: whether to use the new Widget object to update Elementthe configuration of the corresponding object on the old UI tree ; through its source code we can see, as long as newWidgetwith oldWidgetthe runtimeTypeand keywill use simultaneously equal newWidgetto update the Elementconfiguration objects, otherwise it will create a new Element.

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 Widgetclass 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 Widgetclass to implement a new component. On the contrary, we usually implement it through inheritance StatelessWidgetor StatefulWidgetindirect inheritance Widget. Both StatelessWidgetand StatefulWidgetare directly inherited from Widgetclasses, 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.

3.1.4 StatelessWidget

In the previous section, we have introduced a simple StatelessWidget, StatelessWidgetrelatively simple, it inherits from Widgetclass, override the createElement()method:

@override
StatelessElement createElement() => new StatelessElement(this);

StatelessElementIndirectly inherited from the Elementclass, and StatelessWidgetcorresponding (as its configuration data).

StatelessWidgetUsed in scenarios that do not need to maintain state, it usually buildconstructs 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 Echowidget that echoes a string .

By convention, widgetthe constructor parameters should use named parameters, and the necessary parameters in the named parameters should be @requiredmarked, which is conducive to the static code analyzer to check. In addition, when inheriting widget, the first parameter should usually be Key. In addition, if the Widget needs to receive a child Widget, the childor childrenparameter 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 to finalprevent 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:

Figure 3-1

Context

buildThe method has a contextparameter, which is BuildContextan 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 contextis 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:

Figure 3-1-1

Note : For BuildContextreaders, 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. For BuildContextmore content, we will also introduce in-depth later in the advanced part.

3.1.5 StatefulWidget

It is the StatelessWidgetsame, StatefulWidgetbut also inherits from the Widgetclass and overwrites the createElement()method. The difference is that the returned Elementobjects are not the same; in addition StatefulWidget, a new interface is added to the class createState().

Below we look at StatefulWidgetthe class definition:

abstract class StatefulWidget extends Widget {
 const StatefulWidget({ Key key }) : super(key: key);

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

 @protected
 State createState();
}
  • StatefulElementIndirectly inherited from the Elementclass, corresponding to StatefulWidget (as its configuration data). StatefulElementIt may be called multiple times createState()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 a StatefulElementcorresponding 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 StatefulElementhas 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".

3.1.6 State

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:

  1. It can be read synchronously when the widget is built.
  2. 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 its buildmethod to rebuild the widget tree to update the UI. purpose.

There are two common attributes in State:

  1. 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.

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

State life cycle

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();
}

CounterWidgetReceive an initValueinteger 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 initStatemethod 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 initStateand didChangeDependenciesare not called at this time, but didUpdateWidgetare called at this time .

Next, we remove from the widget tree CounterWidgetand change the routing buildmethod 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 CounterWidgetremoved from the widget tree, deactiveand disposewill 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 callback BuildContext.dependOnInheritedWidgetOfExactType(this method is used to get the closest parent to the current widget on the Widget tree InheritFromWidget, InheritedWidgetwe will introduce it in a later chapter), because after the initialization is completed, the Widget tree InheritFromWidgetmay also change , So the correct approach should be to call it in the build()method or didChangeDependencies().
  • didChangeDependencies(): When the object changes dependent State is invoked; for example: before build()containing one InheritedWidget, then after build()the InheritedWidgetchange, then the time InheritedWidgetof the child widget didChangeDependencies()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:
  1. After the call initState().
  2. After the call didUpdateWidget().
  3. After the call setState().
  4. After the call didChangeDependencies().
  5. 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 call Widget.canUpdateto 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 it Widget.canUpdatereturns true. As mentioned before, Widget.canUpdateit will return true when the key and runtimeType of the old and new widgets are equal at the same time, which means it didUpdateWidget()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, the dispose()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:

Figure 3-2

Note : When inheriting and StatefulWidgetrewriting its method, for @mustCallSuperthe parent class method that contains annotations, the parent class method must be called first in the subclass method.

Why put the build method in State instead of StatefulWidget?

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 StatefulWidgethave a lot of states, and each state change has to call the buildmethod, because the state is stored in the State, if the buildmethod is StatefulWidgetin, then the buildmethod and state are in two classes, then read during construction The state will be very inconvenient! Imagine if you really put the buildmethod in the StatefulWidget, since the process of building the user interface needs to rely on State, the buildmethod will have to add a Stateparameter, 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.

  • StatefulWidgetInconvenient inheritance .

For example, there is a base class for animation widget in Flutter AnimatedWidget, which inherits from the StatefulWidgetclass. AnimatedWidgetAn abstract method is introduced in build(BuildContext context), and all AnimatedWidgetthe animation widgets inherited from it must implement this buildmethod. Now imagine that if StatefulWidgetthere is already a buildmethod in the class , as mentioned above, the buildmethod needs to receive a state object, which means that it AnimatedWidgetmust provide its own State object (denoted as _animatedWidgetState) to its subclasses, because The subclass needs buildto call the method of the parent class in its buildmethod, 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

  1. AnimatedWidgetThe state object is AnimatedWidgetan internal implementation detail and should not be exposed to the outside.
  2. 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 StatefulWidgetputting the buildmethod in the State can bring great flexibility to development.

3.1.7 Get the State object in the Widget tree

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 ScaffoldStatedefines 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.

Get through Context

contextThe 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:

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 ofstatic method to obtain its State object, and developers can directly obtain it through this method; if State does not want to be exposed, no ofmethod is provided . This convention can be seen everywhere in the Flutter SDK. Therefore, the above example Scaffoldalso provides a ofmethod, we can actually call it directly:

...//省略无关代码
// 直接通过of静态方法来获取ScaffoldState 
ScaffoldState _state=Scaffold.of(context); 
_state.showSnackBar(
 SnackBar(
   content: Text("我是SnackBar"),
 ),
);

Via GlobalKey

Flutter also has a general Statemethod of obtaining objects-through GlobalKey! There are two steps:

  1. StatefulWidgetAdd to the target GlobalKey.
   //定义一个globalKey, 由于GlobalKey要保持全局唯一性,我们使用静态变量存储
   static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
   ...
   Scaffold(
       key: _globalKey , //设置key
       ...  
   )
   
  1. GlobalKeyGet Stateobjects 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.currentWidgetobtain the globalKey.currentElementelement object corresponding to the widget by obtaining the widget object, and if the current widget is StatefulWidget, we can globalKey.currentStateobtain 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.

3.1.8 Introduction to Flutter SDK built-in component library

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.

Basic 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 Android FrameLayoutlike), Stackallows the child widget stack, you can use Positionedto locate them with respect to the Stackposition 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: ContainerAllows you to create rectangular visual elements. A container can be decorated BoxDecoration, such as a background, a border, or a shadow. ContainerIt can also have margins, padding, and constraints applied to its size. In addition, Containera matrix can be used to transform it in three-dimensional space.

Material components

Flutter provides a rich set of Material components, which can help us build applications that follow Material Design specifications. The Material application MaterialAppstarts with a component, which creates some necessary components at the root of the application, such as Themecomponents, which are used to configure the theme of the application. Whether to use MaterialAppit 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, FlatButtonand the like. To use the Material component, you need to introduce it first:

import 'package:flutter/material.dart';

Cupertino components

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:

Figure 3-3

About the example

Examples of the later chapters of this chapter will use some layout class components, such as Scaffold, Row, Columnetc., these components will "Layout class components," described in detail in a later chapter, the reader is not the first concern.

to sum up

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.dartbecause they have already been introduced internally.