Foreword
The notions of Widget, State and Context in Flutter are ones of the most important concepts that every Flutter developer needs to fully understand.
However, the documentation is huge and this concept is not always clearly explained.
I will explain these notions with my own words and shortcuts, knowing that this might risk to shock some purists, but the real objective of this article to try to clarify the following topics:
- difference of Stateful and Stateless widgets
- what is a Context
- what is a State and how to use it
- relationship between a context and its state object
- InheritedWidget and the way to propagate the information inside a Widgets tree
- notion of rebuild
Cet article est également disponible (en Anglais) sur Medium - Flutter Community.
Part 1: Concepts
Notion of Widget
In Flutter, almost everything is a Widget.
Think of a Widget as a visual component (or a component that interacts with the visual aspect of an application).
When you need to build anything that directly or indirectly is in relation with the layout, you are using Widgets.
Notion of Widgets tree
Widgets are organized in tree structure(s).
A widget that contains other widgets is called parent Widget (or Widget container). Widgets which are contained in a parent Widget are called children Widgets.
Let's illustrate this with the base application which is automatically generated by Flutter. Here is the simplified code, limited to the build method:
1
2
3 Widget build(BuildContext){
4 return Scaffold(
5 appBar: AppBar(
6 title: Text(widget.title),
7 ),
8 body: Center(
9 child: Column(
10 mainAxisAlignment: MainAxisAlignment.center,
11 children: <Widget>[
12 Text(
13 'You have pushed the button this many times:',
14 ),
15 Text(
16 '$_counter',
17 style: Theme.of(context).textTheme.display1,
18 ),
19 ],
20 ),
21 ),
22 floatingActionButton: FloatingActionButton(
23 onPressed: _incrementCounter,
24 tooltip: 'Increment',
25 child: const Icon(Icons.add),
26 ),
27 );
28 }
29
If we now consider this basic example, we obtain the following Widgets tree structure (limited the list of Widgets present in the code):
Notion of Context
Another important notion is the Context.
A context is nothing else but a reference to the location of a Widget within the tree structure of all the Widgets which are built.
In short, think of a context as the part of Widgets tree where the Widget is attached to this tree.
A context only belongs to one widget.
If a widget 'A' has children widgets, the context of widget 'A' will become the parent context of the direct children contexts.
Reading this, it is clear that contexts are chained and are composing a tree of contexts (parent-children relationship).
If we now try to illustrate the notion of Context in the previous diagram, we obtain (still as a very simplified view) where each color represents a context (except the MyApp one, which is different):
Context Visibility (Simplified statement):
Something is only visible within its own context or in the context of its parent(s) context.
From this statement we can derive that from a child context, it is easily possible to find an ancestor (= parent) Widget.
An example is, considering the Scaffold > Center > Column > Text: context.findAncestorWidgetOfExactType(Scaffold) => returns the first Scaffold by going up to tree structure from the Text context.
From a parent context, it is also possible to find a descendant (= child) Widget but it is not advised to do so (we will discuss this later).
Types of Widgets
Widgets are of 2 types:
Stateless Widget
Some of these visual components do not depend on anything else but their own configuration information, which is provided at time of building it by its direct parent.
In other words, these Widgets will not have to care about any variation, once created.
These Widgets are called Stateless Widgets.
Typical examples of such Widgets could be Text, Row, Column, Container... where during the building time, we simply pass some parameters to them.
Parameters might be anything from a decoration, dimensions, or even other widget(s). It does not matter. The only thing which is important is that this configuration, once applied, will not change before the next building process.
A stateless widget can only be drawn once when the Widget is loaded/built, which means that that Widget cannot be redrawn based on any events or user actions.
Stateless Widget lifecycle
Here is a typical structure of the code related to a Stateless Widget.
As you may see, we can pass some additional parameters to its constructor. However, bear in mind that these parameters will NOT change (mutate) at a later stage and have to be only used as is.
1
2 class MyStatelessWidget extends StatelessWidget {
3
4 MyStatelessWidget({
5 super.key,
6 required this.parameter,
7 });
8
9 final parameter;
10
11
12 Widget build(BuildContext context){
13 return ...
14 }
15 }
16
Even if there is another method that could be overridden (createElement), the latter is almost never overridden. The only one that needs to be overridden is build.
The lifecycle of such Stateless widget is straightforward:
- initialization
- rendering via build()
Stateful Widget
Some other Widgets will handle some inner data that will change during the Widget's lifetime. This data hence becomes dynamic.
The set of data held by this Widget and which may vary during the lifetime of this Widget is called a State.
These Widgets are called Stateful Widgets.
An example of such Widget might be a list of Checkboxes that the user can select or a Button which is disabled depending on a condition.
Notion of State
A State defines the "behavioural" part of a StatefulWidget instance.
It holds information aimed at interacting / interferring with the Widget in terms of:
- behaviour
- layout
Any changes which is applied to a State forces the Widget to rebuild.
Relation between a State and a Context
For Stateful widgets, a State is associated with a Context. This association is permanent and the State object will never change its context.
Even if the Widget Context can be moved around the tree structure, the State will remain associated with that context.
When a State is associated with a Context, the State is considered as mounted.
HYPER IMPORTANT:
As a State object is associated with a context, this means that the State object is NOT (directly) accessible through another context ! (we will further discuss this in a few moment).
Stateful Widget lifecycle
Now that the base concepts have been introduced, it is time to dive a bit deeper...
Here is a typical structure of the code related to a Stateful Widget.
As the main objective of this article is to explain the notion of State in terms of "variable" data, I will intentionally skip any explanation related to some Stateful Widget overridable methods, which do not specifically relate to this. These overridable methods are didUpdateWidget, deactivate, reassemble. These will be discussed in a next article.
1
2 class MyStatefulWidget extends StatefulWidget {
3
4 MyStatefulWidget({
5 super.key,
6 required this.parameter,
7 });
8
9 final parameter;
10
11
12 _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
13 }
14
15 class _MyStatefulWidgetState extends State<MyStatefulWidget> {
16
17
18 void initState(){
19 super.initState();
20
21 // Additional initialization of the State
22 }
23
24
25 void didChangeDependencies(){
26 super.didChangeDependencies();
27
28 // Additional code
29 }
30
31
32 void dispose(){
33 // Additional disposal code
34
35 super.dispose();
36 }
37
38
39 Widget build(BuildContext context){
40 return ...
41 }
42 }
43
The following diagram shows (a simplified version of) the sequence of actions/calls related to the creation of a Stateful Widget. At the right side of the diagram, you will notice the inner status of the State object during the flow. You will also see the moment when the context is associated with the state, and thus becomes available (mounted).
So let's explain it with some additional details:
initState()
The initState() method is the very first method (after the constructor) to be called once the State object has been created. This method is to be overridden when you need to perform additional initializations. Typical initializations are related to animations, controllers... If you override this method, you need to call the super.initState() method and normally at first place.
In this method, a context is available but you cannot really use it yet since the framework has not yet fully associated the state with it.
Once the initState() method is complete, the State object is now initialized and the context, available.
This method will not be invoked anymore during the lifetime of this State object.
didChangeDependencies()
The didChangeDependencies() method is the second method to be invoked.
At this stage, as the context is available, you may use it.
This method is usually overridden if your Widget is linked to an InheritedWidget and/or if you need to initialize some listeners (based on the context).
Note that if your widget is linked to an InheritedWidget, this method will be called each time this Widget will be rebuilt.
If you override this method, you should invoke the super.didChangeDependencies() at first place.
build()
The build(BuildContext context) method is called after the didChangeDependencies() (and didUpdateWidget).
This is the place where you build your widget (and potentially any sub-tree).
This method will be called each time your State object changes (or when an InheritedWidget needs to notify the "registered" widgets) !!
In order to force a rebuild, you may invoke setState((){...}) method.
dispose()
The dispose() method is called when the widget is discarded.
Override this method if you need to perform some cleanup (e.g. listeners), then invoke the super.dispose() right after.
Stateless or Stateful Widget?
This is a question that many developers need to ask themselves: do I need my Widget to be Stateless or Stateful?
In order to answer this question, ask yourself:
In the lifetime of my widget, do I need to consider a variable that will change and when changed, will force the widget to be rebuilt?
If the answer to the question is yes, then you need a Stateful widget, otherwise, you need a Stateless widget.
Some examples:
a widget to display a list of checkboxes. To display the checkboxes, you need to consider an array of items. Each item is an object with a title and a status. If you click on a checkbox, the corresponding item.status is toggled;
In this case, you need to use a Stateful widget to remember the status of the items to be able to redraw the checkboxes.
a screen with a Form. The screen allows the user to fill the Widgets of the Form and send the form to the server.
In this case, unless you need to validate the Form or do any other action before submitting it, a Stateless widget might be enough.
Stateful Widget is made of 2 parts
Remember the structure of a Stateful widget? There are 2 parts:
The Widget main definition
1
2 class MyStatefulWidget extends StatefulWidget {
3 const MyStatefulWidget({
4 super.key,
5 required this.color,
6 });
7
8 final Color color;
9
10
11 _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
12 }
13
The first part "MyStatefulWidget" is normally the public part of the Widget. You instantiate this part when you want to add it to a widget tree. This part does not vary during the lifetime of the Widget but may accept parameters that could be used by its corresponding State instance.
Note that any variable, defined at the level of this first part of the Widget will normally NOT change during its lifetime.
The Widget State definition
1
2class _MyStatefulWidgetState extends State<MyStatefulWidget> {
3 ...
4
5 Widget build(BuildContext context){
6 ...
7 }
8}
9
The second part "_MyStatefulWidgetState" is the part which varies during the lifetime of the Widget and forces this specific instance of the Widget to rebuild each time a modification is applied. The '_' character in the beginning of the name makes the class private to the .dart file.
If you need to make a reference to this class outside the .dart file, do not use the '_' prefix.
The _MyStatefulWidgetState class can access any variable which is stored in the MyStatefulWidget, using widget.{name of the variable}. In this example: widget.color
Widget unique identity - Key
In Flutter, each Widget is uniquely identified. This unique identity is defined by the framework at build/rendering time.
This unique identity corresponds to the optional Key parameter. If omitted, Flutter will generate one for you.
In some circumstances, you might need to force this key, so that you can access a widget by its key.
To do so, you can use one of the following helpers: GlobalKey<T>, LocalKey, UniqueKey or ObjectKey.
The GlobalKey ensures that the key is unique across the whole application.
To force a unique identity of a Widget:
1
2GlobalKey myKey = GlobalKey();
3...
4
5Widget build(BuildContext context){
6 return MyWidget(
7 key: myKey
8 );
9 }
10
Part 2: How to access the State?
As previously explained, a State is linked to one Context and a Context is linked to an instance of a Widget.
1. The Widget itself
In theory, the only one which is able to access a State is the Widget State itself.
In this case, there is no difficulty. The Widget State class accesses any of its variables.
2. A direct child Widget
Sometimes, a parent widget might need to get access to the State of one of its direct children to perform specific tasks.
In this case, to access these direct children State, you need to know them.
The easiest way to call somebody is via a name. In Flutter, each Widget has a unique identity, which is determined at build/rendering time by the framework. As shown earlier, you may force the identity of a Widget, using the key parameter.
1
2...
3GlobalKey<MyStatefulWidgetState> myWidgetStateKey = GlobalKey<MyStatefulWidgetState>();
4...
5
6Widget build(BuildContext context){
7 return MyStatefulWidget(
8 key: myWidgetStateKey,
9 color: Colors.blue,
10 );
11 }
12
Once identified, a parent Widget might access the State of its child via:
myWidgetStateKey.currentState
Let's consider a basic example that opens the Drawer when the user hits a button. As the Drawer is a child Widget of the Scaffold it is not directly accessible to any other child of the body of the Scaffold (remember the notion of context and its hierarchy/tree structure ?). Therefore, the only way to access it, is via the ScaffoldState, which exposes a public method to open the Drawer.
1
2class _MyScreenState extends State<MyScreen> {
3 /// the unique identity of the Scaffold
4 final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
5
6
7 Widget build(BuildContext context){
8 return Scaffold(
9 key: _scaffoldKey,
10 appBar: AppBar(
11 title: Text('My Screen'),
12 ),
13 drawer: Drawer(),
14 body: Center(
15 RaiseButton(
16 child: Text('Hit me'),
17 onPressed: (){
18 _scaffoldKey.currentState?.openDrawer();
19 }
20 ),
21 ),
22 );
23 }
24}
25
3. Ancestor Widget
Suppose that you have a Widget that belongs to a sub-tree of another Widget as shown in the following diagram.
3 conditions need to be met to make this possible:
1. the "Widget with State" (in red) needs to expose its State
In order to expose its State, the Widget needs to record it at time of creation, as follows:
1
2class MyExposingWidget extends StatefulWidget {
3
4 late MyExposingWidgetState myState;
5
6
7 MyExposingWidgetState createState(){
8 myState = MyExposingWidgetState();
9 return myState;
10 }
11}
12
2. the "Widget State" needs to expose some getters/setters
In order to let a "stranger" to set/get a property of the State, the Widget State needs to authorize the access, through:
- public property (not recommended)
- getter / setter
Example:
1
2class MyExposingWidgetState extends State<MyExposingWidget>{
3 Color _color;
4
5 Color get color => _color;
6 ...
7}
8
3. the "Widget interested in getting the State" (in blue) needs to get a reference to the State
1
2class MyChildWidget extends StatelessWidget {
3
4 Widget build(BuildContext context){
5 final MyExposingWidget? widget = context.findAncestorWidgetOfExactType(MyExposingWidget);
6 final MyExposingWidgetState? state = widget?.myState;
7
8 return Container(
9 color: state == null ? Colors.blue : state.color,
10 );
11 }
12}
13
This solution is easy to implement but how does the child widget know when it needs to rebuild?
With this solution, it does not. It will have to wait for a rebuild to happen to refresh its content, which is not very convenient.
The next section tackles the notion of Inherited Widget which gives a solution to this problem.
InheritedWidget
In short and with simple words, the InheritedWidget allows to efficiently propagate (and share) information down a tree of widgets.
The InheritedWidget is a special Widget, that you put in the Widgets tree as a parent of another sub-tree. All widgets part of that sub-tree will have to ability to interact with the data which is exposed by that InheritedWidget.
Basics
In order to explain it, let's consider the following piece of code:
1
2class MyInheritedWidget extends InheritedWidget {
3 MyInheritedWidget({
4 Key? key,
5 required Widget child,
6 this.data,
7 }): super(key: key, child: child);
8
9 final data;
10
11 static MyInheritedWidget? of(BuildContext context) {
12 return context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
13 }
14
15
16 bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
17}
18
This code defines a Widget, named "MyInheritedWidget", aimed at "sharing" some data across all widgets, part of the child sub-tree.
As mentioned earlier, an InheritedWidget needs to be positioned at the top of a widgets tree in order to be able to propagate/share some data, this explains the required Widget child which is passed to the InheritedWidget base constructor.
The static MyInheritedWidget of(BuildContext context) method, allows all the children widgets to get the instance of the closest MyInheritedWidget which encloses the context (see later).
Finally the updateShouldNotify overridden method is used to tell the InheritedWidget whether notifications will have to be passed to all the children widgets (that registered/subscribed) if a modification be applied to the data (see later).
Therefore, we need to put it at a tree node level as follows:
1
2class MyParentWidget... {
3 ...
4
5 Widget build(BuildContext context){
6 return MyInheritedWidget(
7 data: counter,
8 child: Row(
9 children: <Widget>[
10 ...
11 ],
12 ),
13 );
14 }
15}
16
How does a child get access to the data of the InheritedWidget?
At time of building a child, the latter will get a reference to the InheritedWidget, as follows:
1
2class MyChildWidget... {
3 ...
4
5
6 Widget build(BuildContext context){
7 final MyInheritedWidget? inheritedWidget = MyInheritedWidget.of(context);
8
9 ///
10 /// From this moment, the widget can use the data, exposed by the MyInheritedWidget
11 /// by calling: inheritedWidget.data
12 ///
13 return Container(
14 color: inheritedWidget?.data.color,
15 );
16 }
17}
18
How to make interactions between Widgets?
Consider the following diagram that shows a widgets tree structure.
In order to illustrate a type of interaction, let's suppose the following:
- 'Widget A' is a button that adds an item to the shopping cart;
- 'Widget B' is a Text that displays the number of items in the shopping cart;
- 'Widget C' is next to Widget B and is a Text with any text inside;
- We want the 'Widget B' to automatically display the right number of items in the shopping cart, as soon as the 'Widget A' is pressed but we do not want 'Widget C' to be rebuilt
The InheritedWidget is just the right Widget to use for that!
Example by the code
Let's first write the code and explanations will follow:
1
2class Item {
3 Item(this.reference);
4
5 String reference;
6 }
7
8 class _MyInherited extends InheritedWidget {
9 const _MyInherited({
10 Key? key,
11 required Widget child,
12 required this.data,
13 }) : super(key: key, child: child);
14
15 final MyInheritedWidgetState data;
16
17
18 bool updateShouldNotify(_MyInherited oldWidget) => true;
19 }
20
21 class MyInheritedWidget extends StatefulWidget {
22 const MyInheritedWidget({
23 super.key,
24 required this.child,
25 });
26
27 final Widget child;
28
29
30 MyInheritedWidgetState createState() => MyInheritedWidgetState();
31
32 static MyInheritedWidgetState? of(BuildContext context) {
33 return (context.dependOnInheritedWidgetOfExactType<_MyInherited>())
34 ?.data;
35 }
36 }
37
38 class MyInheritedWidgetState extends State<MyInheritedWidget> {
39 // List of Items
40 final List<Item> _items = <Item>[];
41
42 // Getter (number of items)
43 int get itemsCount => _items.length;
44
45 // Helper method to add an Item
46 void addItem(String reference) {
47 if (mounted) {
48 setState(() {
49 _items.add(Item(reference));
50 });
51 }
52 }
53
54
55 Widget build(BuildContext context) {
56 return _MyInherited(
57 data: this,
58 child: widget.child,
59 );
60 }
61 }
62
63 class MyTree extends StatefulWidget {
64 const MyTree({super.key});
65
66
67 _MyTreeState createState() => _MyTreeState();
68 }
69
70 class _MyTreeState extends State<MyTree> {
71
72 Widget build(BuildContext context) {
73 return MyInheritedWidget(
74 child: Scaffold(
75 appBar: AppBar(
76 title: const Text('Title'),
77 ),
78 body: Column(
79 children: <Widget>[
80 const WidgetA(),
81 Row(
82 children: const <Widget>[
83 Icon(Icons.shopping_cart),
84 WidgetB(),
85 WidgetC(),
86 ],
87 ),
88 ],
89 ),
90 ),
91 );
92 }
93 }
94
95 class WidgetA extends StatelessWidget {
96 const WidgetA({super.key});
97
98
99 Widget build(BuildContext context) {
100 final MyInheritedWidgetState? state = MyInheritedWidget.of(context);
101 return ElevatedButton(
102 child: const Text('Add Item'),
103 onPressed: () {
104 state?.addItem('new item');
105 },
106 );
107 }
108 }
109
110 class WidgetB extends StatelessWidget {
111 const WidgetB({super.key});
112
113
114 Widget build(BuildContext context) {
115 final MyInheritedWidgetState? state = MyInheritedWidget.of(context);
116 return Text('${state?.itemsCount}');
117 }
118 }
119
120 class WidgetC extends StatelessWidget {
121 const WidgetC({super.key});
122
123
124 Widget build(BuildContext context) {
125 return const Text('I am Widget C');
126 }
127 }
128
Explanations
In this very basic example,
- _MyInherited is an InheritedWidget that is recreated each time we add an Item via a click on the button of 'Widget A'
- MyInheritedWidget is a Widget with a State that contains the list of Items. This State is accessible via the static MyInheritedWidgetState of(BuildContext context)
- MyInheritedWidgetState exposes one getter (itemsCount) and one method (addItem) so that they will be usable by the widgets, part of the child widgets tree
- Each time we add an Item to the State, the MyInheritedWidgetState rebuilds
- MyTree class simply builds a widgets tree, having the MyInheritedWidget as parent of the tree
- WidgetA is a simple ElevatedButton which, when pressed, invokes the addItem method from the closest MyInheritedWidget
- WidgetB is a simple Text which displays the number of items, present at the level of the closest MyInheritedWidget
How does all this work?
Registration of a Widget for later notifications
When a child Widget invokes the MyInheritedWidget.of(context), it makes a call to the following method of MyInheritedWidget, passing its own context.
1
2static MyInheritedWidgetState? of(BuildContext context) {
3 return (context.dependOnInheritedWidgetOfExactType<_MyInherited>())
4 ?.data;
5 }
6
Internally, on top of simply returning the instance of MyInheritedWidgetState, it also subscribes the consumer widget to the changes notifications.
Behind the scene, the simple call to this static method actually does 2 things:
- the consumer widget is automatically added to the list of subscribers that will be rebuilt when a modification is applied to the InheritedWidget (here _MyInherited)
- the data referenced in the _MyInherited widget (aka MyInheritedWidgetState) is returned to the consumer
Flow
Since both 'Widget A' and 'Widget B' have subscribed with the InheritedWidget so that if a modification is applied to the _MyInherited, the flow of operations is the following (simplified version) when the ElevatedButton of Widget A is clicked:
- A call is made to the addItem method of MyInheritedWidgetState
- MyInheritedWidgetState.addItem method adds a new Item to the List<Item>
- setState() is invoked in order to rebuild the MyInheritedWidget
- A new instance of _MyInherited is created with the new content of the List<Item>
- _MyInherited records the new State which is passed in argument (data)
- As an InheritedWidget, it checks whether there is a need to notify the consumers (answer is true)
- It iterates the whole list of consumers (here Widget A and Widget B) and requests them to rebuild
- As Wiget C is not a consumer, it is not rebuilt.
So it works !
However, both Widget A and Widget B are rebuilt while it is useless to rebuild Wiget A since nothing changed for it. How to prevent this from happening?
Prevent some Widgets from rebuilding while still accessing the Inherited Widget
The reason why Widget A was also rebuilt comes from the way it accesses the MyInheritedWidgetState.
As we saw earlier, the fact of invoking the context.inheritFromWidgetOfExactType() method automatically subscribed the Widget to the list of consumers.
The solution to prevent this automatic subscription while still allowing the Widget A access the MyInheritedWidgetState is to change the static method of MyInheritedWidget as follows:
1
2static MyInheritedWidgetState of(BuildContext context, [bool rebuild = true]){
3 return (rebuild
4 ? context.dependOnInheritedWidgetOfExactType<_MyInherited>()
5 : context.findAncestorWidgetOfExactType<_MyInherited>())
6 ?.data;
7}
8
By adding a boolean extra parameter...
- If the rebuild parameter is true (by default), we use the normal approach (and the Widget will be added to the list of subscribers)
- If the rebuild parameter is false, we still get access to the data but without using the internal implementation of the InheritedWidget
So, to complete the solution, we also need to slightly update the code of Widget A as follows (we add the false extra parameter):
1
2class WidgetA extends StatelessWidget {
3
4 Widget build(BuildContext context) {
5 final MyInheritedWidgetState? state = MyInheritedWidget.of(context, false);
6 return Container(
7 child: ElevatedButton(
8 child: cont Text('Add Item'),
9 onPressed: () {
10 state?.addItem('new item');
11 },
12 ),
13 );
14 }
15}
16
There it is, Widget A is no longer rebuilt when we press it.
Special note for Routes, Dialogs...
Routes, Dialogs contexts are tied to the Application.
This means that even if inside a Screen A you request to display another Screen B (on top of the current, for example), there is no easy way from any of the 2 screens to relate their own contexts.
The only way for Screen B to know anything about the context of Screen A is to obtain it from Screen A as parameter of Navigator.of(context).push(....)
Interesting links
Conclusions
There is still so much to say on these topics... especially on InheritedWidget.
In a next article I will introduce the notion of Notifiers / Listeners which is also very interesting to use in the context of State and the way of conveying data.
So, stay tuned and happy coding.
Didier,