- Widget-state-context-inheritedWidget
- Original author: www.didierboelens.com
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: nanjingboy
- Proofreader: Mirosalva, HearFishle
This article covers important concepts related to Widgets, State, Context, and inheritedWidgets in Flutter applications. Because the InheritedWidget is one of the most important and document-poor widgets, it needs special attention.
Difficulty: Beginner
preface
The widgets, states, and Context of Flutter are among the most important concepts that every Flutter developer needs to fully understand.
Plenty of documentation exists, but none clearly explains it.
I’m going to explain these concepts in my own words, knowing that this may upset some purists, but the real purpose of this article is to try to clarify the following themes:
- The difference between Stateful Widgets and Stateless widgets
- What is the Context
- What is State and how is it used
- The relationship between the context and its state object
- Inheritedwidgets and the way they propagate information in the Widgets tree
- The concept of reconstruction
This article was also published in the Medium – Flutter Community.
Part ONE: Concept
The concept of the Widget
Almost everything in Flutter is a Widget.
Think of a Widget as a visual component (or one that interacts with visual aspects of your application).
You are using widgets when you need to build anything directly or indirectly related to your layout.
The concept of Widget trees
Widgets are organized in a tree structure.
Widgets that contain other widgets are called parent widgets (or Widget containers). Widgets that are included in the parent Widget are called child widgets.
Let’s illustrate this with the basic application of Flutter auto-generation. Here is the simplified code, with only the build method:
@override
Widget build(BuildContext){
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',),new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
Copy the code
If we now look at the basic example, we get the following Widget tree structure (limiting the list of widgets that exist in the code) :
The concept of Context
Another important concept is Context.
The Context is simply a reference to the location of a Widget in the Widget tree structure that has been created.
In short, you treat the context as part of the Widget tree to which the widgets corresponding to the context are added.
A context is only subordinate to a widget.
If Widget ‘A’ has A child widget, the context of widget ‘A’ becomes the parent of its directly associated child context.
Reading this, it is obvious that the context is linked together and forms a context tree (parent-child relationship).
If we now try to illustrate the concept of Context in the figure above, we get (again a very simplified view) that each color represents a Context (except for MyApp, which is different) :
Context visibility (short description) :
Something can only be seen in its own context or in its parent context.
From the description above we can extract it from the child context and it is easy to find an ancestor (= parent) Widget.
For an example, consider Scaffold > Center > Column > Text: Context. AncestorWidgetOfExactType (Scaffold) = > tree structure is obtained by from the context of the Text to return the first Scaffold.
Descendant (= child) widgets can also be found from the parent context, but this is not recommended (we’ll discuss this later).
The type of Widget
Widgets come in two types:
Stateless Widget
These visual components do not depend on any information other than their own configuration information, which is provided when their immediate parent node is built.
In other words, these widgets don’t care about any changes once they are created.
Such widgets are called Stateless widgets.
Typical examples of such widgets are Text, Row, Column, Container, and so on. At build time, we just need to pass some parameters to them.
The parameters can be decoration, size, or even anything in other widgets. It is important to emphasize that this configuration, once created, will not change until the next build process.
The Stateless widget is only drawn once when loaded/built, which means that no event or user action can redraw the widget.
Stateless Widget lifecycle
The following is a typical code structure associated with Stateless widgets.
As shown below, we can pass some extra arguments to its constructor. Keep in mind, however, that these parameters do not change (change) in subsequent phases and must be used in the existing state.
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({
Key key,
this.parameter,
}): super(key:key);
final parameter;
@override
Widget build(BuildContext context){
return new. }}Copy the code
Even if there is another method that can be overridden (createElement), the latter is almost never overridden. The only thing that needs to be overridden is the build method.
The life cycle of this Stateless Widget is fairly simple:
- Initialize the
- Render by build()
Stateful Widget
Other widgets handle internal data that changes over the lifetime of the Widget. Therefore, such data becomes dynamic.
The Widget holds a data set that may change during its lifetime, and such data is called State.
These widgets are called Stateful widgets.
An example of such a Widget might be a list of check boxes that a user can select, or a Button Button that is disabled based on conditions.
The concept of the State
State defines the “behavior” of StatefulWidget instances.
It contains information for interaction/intervention widgets:
- behavior
- layout
Any changes applied to State force the Widget to rebuild.
The relationship between State and Context
For Stateful widgets, State is associated with Context. And the association is permanent; the State object will never change its context.
Even if the Widget Context can be moved around the tree structure, State will still be associated with that Context.
When State is associated with Context, State is considered mounted.
Key points:
A State object is associated with a context, which means that the State object does not access another context! (We’ll discuss this later).
Stateful Widget life cycle
Now that you’ve introduced the basic concepts, it’s time to dig a little deeper…
The following is a typical code structure related to Stateful Widgets.
Since the main purpose of this article is to explain the concept of State in terms of “variable” data, I will deliberately skip any explanation of some overridden methods related to Stateful widgets that have no particular relevance to this. These rewritable methods are didUpdateWidget, deactivate and Reassemble. These will be discussed in the next article.
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.parameter,
}): super(key: key);
final parameter;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
void initState(){
super.initState();
// Additional initialization of the State
}
@override
void didChangeDependencies(){
super.didChangeDependencies();
// Additional code
}
@override
void dispose(){
// Additional disposal code
super.dispose();
}
@override
Widget build(BuildContext context){
return new. }}Copy the code
The following figure shows the sequence of actions/calls related to creating Stateful Widgets (simplified version). On the right side of the diagram, you will notice the internal State of the State object in the data flow. Note that the context is associated with state, and the context becomes available (Mounted).
Let’s explain it with some extra detail:
initState()
Once the State object is created, the initState() method is the first method called (after the constructor). This method will be overridden when you need to perform additional initialization. Common initializations are related to animations, controllers, and so on. If you override this method, you should first call super.initstate ().
This method can get the context, but can’t really use it because the framework hasn’t fully associated it with state yet.
Once the initState() method completes, the State object is initialized and the context becomes available.
This method will not be called again during the life of the State object.
didChangeDependencies()
The didChangeDependencies() method is the second method called.
At this stage, since the context is available, you can use it.
This method is often overridden if your Widget links to an InheritedWidget and/or if you need to initialize some context based process.
Note that if your widget is linked to an InheritedWidget, this method is called every time you rebuild the widget.
If you rewrite this method, you should first call the super. DidChangeDependencies ().
build()
The Build (BuildContext Context) method is called after didChangeDependencies() (and didUpdateWidget).
This is where you build your widget (and possibly any subtrees).
This method is called every time the State object is updated (or when the InheritedWidget needs to be notified of the “registered” widget)!!
To force a rebuild, you may need to call setState((){… }) method.
dispose()
The Dispose () method is called when the widget is deprecated.
If you need to do some cleanup (e.g. listeners), override this method and call super.dispose() immediately afterwards.
Stateless or Stateful Widget?
This is a question many developers need to ask themselves: Do I want widgets to be Stateless or Stateful?
To answer this question, please ask yourself:
During the life cycle of my widget, do I need to consider a variable that will change and that the widget will force a rebuild after the change?
If the answer to the question is yes, you need a Stateful Widget; otherwise, you need a Stateless Widget.
Some examples:
-
A widget that displays a list of check boxes. To display checkboxes, you need to consider a number of items. Each item is an object containing a title and status. If you click a check box, the corresponding item.status will switch;
In this case, you need to use a Stateful Widget to remember the state of the item in order to be able to redraw the checkbox.
-
Screen with tables. This screen allows the user to fill in the Widget of the form and send the form to the server.
In this case, unless you want to validate the form or do something else before submitting it, a Stateless Widget might suffice.
Stateful Widget consists of two parts
Remember the architecture of Stateful Widgets? There are 2 parts:
The Widget definition
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
Copy the code
The first part, “MyStatefulWidget,” is usually the public part of the Widget. You can instantiate it when you need to add it to the Widget tree. This section does not change over the lifetime of the Widget, but may accept the parameters used in its associated State instantiation.
Note that any variables defined in the first part of the Widget generally do not change during its lifetime.
The Widget State definition
class _MyStatefulWidgetState extends State<MyStatefulWidget> {...@overrideWidget build(BuildContext context){ ... }}Copy the code
The second part, “_MyStatefulWidgetStat,” manages changes in the Widget’s life cycle and forces the Widget instance to be rebuilt each time a change is applied. The ‘_’ character at the beginning of the name makes the class private to the.dart file.
If you need to reference this class outside of the.dart file, do not use the ‘_’ prefix.
The _MyStatefulWidgetState class can access any variables stored in MyStatefulWidget by using widget.{variable name}. In this example: widget.color.
Unique id of the Widget – Key
In Fultter, each Widget is uniquely identified. This unique identifier is defined by the frame during build/ Rendering.
This unique identifier corresponds to the optional Key parameter. If you omit this parameter, Flutter will generate one for you.
In some cases, you may need to force this key so that the widget can be accessed through its key.
To do this, you can use any of the following methods: GlobalKey, LocalKey, UniqueKey, or ObjectKey.
GlobalKey ensures that the generated key is unique throughout the application.
Enforce unique identifiers for widgets:
GlobalKey myKey = newGlobalKey(); .@override
Widget build(BuildContext context){
return new MyWidget(
key: myKey
);
}
Copy the code
Part 2: How do I access State?
As mentioned earlier, State is linked to a Context, and a Context is linked to a Widget instance.
1. The Widget itself
Theoretically, the only thing that can access State is Widget State itself.
There is no difficulty in this case. The Widget State class can access any internal variable.
2. Direct child widgets
Sometimes, the parent widget may need to access the State of its immediate child node to perform a specific task.
In this case, to access the State of these immediate child nodes, you need to know about them.
The easiest way to call someone is by name. In Flutter, each Widget has a unique identity that is determined by the frame during build/rendering. As shown earlier, you can use the key parameter to force an identity for the Widget.
. GlobalKey<MyStatefulWidgetState> myWidgetStateKey =newGlobalKey<MyStatefulWidgetState>(); .@override
Widget build(BuildContext context){
return new MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
Copy the code
Once identified, the parent Widget can access the State of its child nodes in the following form:
myWidgetStateKey.currentState
Let’s consider the basic example of displaying a SnackBar when the user clicks a button. Because SnackBar is a child Widget of Scaffold, it is not directly accessible by any other children of Scaffold (remember the concept of context and its hierarchy/tree structure?). . Therefore, the only way to access it is through ScaffoldState, which exposes a public method to display SnackBar.
class _MyScreenState extends State<MyScreen> {
/// the unique identity of the Scaffold
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context){
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('My Screen'),
),
body: new Center(
new RaiseButton(
child: new Text('Hit me'),
onPressed: (){
_scaffoldKey.currentState.showSnackBar(
new SnackBar(
content: new Text('This is the Snackbar... '))); }),),); }}Copy the code
3. Ancestors Widget
Suppose you have a Widget that belongs to a subtree of another Widget, as shown in the figure below.
In order to achieve this, three conditions need to be met:
1.”Widget with State“(red) needs to be exposedState
To expose its State, the Widget needs to record it at creation time, as shown below:
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = new MyExposingWidgetState();
returnmyState; }}Copy the code
2.”Widget State“Need to expose some getter/setter methods
In order for “other classes” to set/get properties in State, Widget State needs to be granted access by:
- Public Attributes (not recommended)
- getter / setter
Example:
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color getcolor => _color; . }Copy the code
3.”Widgets interested in getting State“(blue) need to getStateA reference to the
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
finalMyExposingWidgetState state = widget? .myState;return new Container(
color: state == null? Colors.blue : state.color, ); }}Copy the code
This solution is easy to implement, but how does the child widget know when it needs to be rebuilt?
Through this scheme, it is powerless. It must wait until the rebuild occurs to refresh its contents, which is not particularly convenient.
The next section discusses the concept of Inherited Widgets, which solves this problem.
InheritedWidget
In short, inheritedWidgets allow for efficient propagation (and sharing) of information down the widget tree.
InheritedWidget is a special Widget that is placed in the Widget tree as a parent of another child tree. All widgets of the subtree must be able to interact with the data exposed by the InheritedWidget.
basis
To explain it, let’s consider the following code snippet:
class MyInheritedWidget extends InheritedWidget {
MyInheritedWidget({
Key key,
@required Widget child,
this.data,
}): super(key: key, child: child);
final data;
static MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
}
@override
boolupdateShouldNotify(MyInheritedWidget oldWidget) => data ! = oldWidget.data; }Copy the code
The code defines a Widget called “MyInheritedWidget” that aims to provide some “shared” data for all the widgets in the subtree.
As mentioned earlier, in order to be able to propagate/share some data, the InheritedWidget needs to be placed at the top of the Widget tree, which explains the @Required Widget Child parameter passed to the InheritedWidget base constructor.
The static MyInheritedWidget of(BuildContext Context) method allows all child widgets to get the most recent Instance of MyInheritedWidget from the contained context (see below).
Finally, the updateShouldNotify method is overridden to tell the InheritedWidget whether notification must be passed to all child widgets (registered/subscribed) if changes are made to the data (see below).
Therefore, we need to place it at the tree node level, as follows:
class MyParentWidget.{...@override
Widget build(BuildContext context){
return new MyInheritedWidget(
data: counter,
child: newRow( children: <Widget>[ ... ] ,),); }}Copy the code
How do child nodes access the InheritedWidget’s data?
When a child node is built, the latter gets a reference to the InheritedWidget, as shown below:
class MyChildWidget.{...@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
///
/// At this point, the widget can use the data exposed by the MyInheritedWidget
/// by calling inheritedWidget.data
///
return newContainer( color: inheritedWidget.data.color, ); }}Copy the code
How do I interact with widgets?
Consider the widget tree structure shown in the figure below.
To illustrate the interaction, we make the following assumptions:
- ‘Widget A’ is A button that adds items to the shopping cart;
- ‘Widget B’ is a text that displays the number of items in the shopping cart;
- ‘Widget C’, next to Widget B, is a text with arbitrary text built in;
- We want ‘Widget A’ to automatically display the correct number of items in the shopping cart when ‘Widget B’ is pressed, but we don’t want to recreate ‘Widget C’
For this scenario, the InheritedWidget is the only appropriate Widget option!
The sample code
Let’s write the code first and then explain it:
class Item {
String reference;
Item(this.reference);
}
class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final MyInheritedWidgetState data;
@override
bool updateShouldNotify(_MyInherited oldWidget) {
return true; }}class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key);
final Widget child;
@override
MyInheritedWidgetState createState() => new MyInheritedWidgetState();
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as_MyInherited).data; }}class MyInheritedWidgetState extends State<MyInheritedWidget>{
/// List of Items
List<Item> _items = <Item>[];
/// Getter (number of items)
int get itemsCount => _items.length;
/// Helper method to add an Item
void addItem(String reference){
setState((){
_items.add(new Item(reference));
});
}
@override
Widget build(BuildContext context){
return new _MyInherited(
data: this, child: widget.child, ); }}class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => new _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(
child: new Scaffold(
appBar: new AppBar(
title: new Text('Title'),
),
body: new Column(
children: <Widget>[
new WidgetA(),
new Container(
child: new Row(
children: <Widget>[
new Icon(Icons.shopping_cart),
new WidgetB(),
newWidgetC(), ], ), ), ], ), ), ); }}class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item'); },),); }}class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Text('${state.itemsCount}'); }}class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text('I am Widget C'); }}Copy the code
instructions
In this very basic example:
- _MyInherited is an InheritedWidget that is recreated every time we add an item with the ‘Widget A’ button
- MyInheritedWidget is a Widget whose State contains a list of items. This State can be accessed using static MyInheritedWidgetState of(BuildContext Context)
- MyInheritedWidgetState exposes a getter method to get itemsCount and an addItem method so that they can be used by widgets, which are part of the child Widget tree
- Every time we add a project to State, MyInheritedWidgetState is rebuilt
- The MyTree class just builds a widget tree and uses the MyInheritedWidget as the root node of the tree
- WidgetA is a simple RaisedButton that, when pressed, invokes the addItem method from the nearest MyInheritedWidget instance
- WidgetB is a simple Text that displays the number of items at the nearest level of MyInheritedWidget
How does it all work?
Register the Widget for subsequent notifications
When a child Widget calls myInheritedWidget.of (context), it passes its own context and calls the following methods of the MyInheritedWidget.
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
Copy the code
Internally, in addition to simply returning the MyInheritedWidgetState real exception, it subscribes to the consumer widget for use in notifying changes.
Behind the scenes, a simple call to this static method actually does two things:
- The consumer widget is automatically added to the subscriber list so that when modifications are applied to the InheritedWidget (in this case, _MyInherited), the widget can be rebuilt
- The data referenced in the _MyInherited Widget (also known as MyInheritedWidgetState) is returned to the consumer
process
Since both ‘Widget A’ and ‘Widget B’ are subscribed to using the InheritedWidget, when clicking on the RaisedButton of Widget A, The operation process is as follows (simplified version) :
- Call the addItem method of MyInheritedWidgetState
- _MyInheritedWidgetState. AddItem method will be added to the list of new projects
- Call setState() to rebuild the MyInheritedWidget
- Create a _MyInherited new instance from the new contents in the list
- _MyInherited Records the new State passed through the data parameter
- As an InheritedWidget, it checks if the consumer needs to be notified (the answer is true)
- Iterate through the entire consumer list (in this case, Widget A and Widget B) and ask them to rebuild
- Since Wiget C is not a consumer, it will not be rebuilt.
So far it works!
However, both Widget A and Widget B have been rebuilt, but since nothing has changed with Wiget A, it does not need to be rebuilt. So how do you prevent this from happening?
Prevent the rebuilding of some widgets while continuing to access Inherited Widgets
Widget A is also rebuilt because of the way it accesses MyInheritedWidgetState.
As mentioned earlier, called the context. InheritFromWidgetOfExactType () method actually will automatically Widget subscribe to the list of customers.
The solution to avoiding automatic subscription while still allowing Widget A to access MyInheritedWidgetState is to modify the static methods of MyInheritedWidget in the following way:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]) {return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
Copy the code
By adding an extra parameter of type Boolean…
- If the rebuild parameter is true (the default), we use the normal method (and add the Widget to the subscriber list)
- If the rebuild parameter is false, we can still access the data, but without using the internal implementation of the InheritedWidget
Therefore, to complete this scenario, we also need to modify Widget A’s code slightly as follows (we add an extra parameter with A value of false) :
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item'); },),); }}Copy the code
That’s it. When we press Widget A, it doesn’t rebuild anymore.
Routes, Dialogs…
Context of Routes and Dialogs is bound to Application.
This means that even if you ask to display another screen B inside screen A (for example, on the current screen), you can’t easily associate their own context from either screen.
The only way screen B wants to know screen A’s context is to get it from screen A and pass it as an argument to navigator.of (context).push(… .).
Interesting links
- Maksim Ryzhikov
- Chema Molins
- The official documentation
- Google I/O 2018 Video
- Scoped_Model
conclusion
There is much more to say on these subjects… Especially on InheritedWidget.
In the next article I’ll introduce the concept of Notifiers/Listeners, which are also interesting in the way they use State and data delivery.
So stay tuned and happy code.
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.