InheritedWidget
helps you to propagate data to the widgets down the widget tree. If you wrap your widgets within the Inherited Widget, all the widgets that are below the Inherited Widget can get its data easily. You can also do this using simple constructor through. But it would become very tedious if the widget level goes very deep. Consider the following example where we have class A, B and C. The data that we give it to the class A, passes that data to the class B, and the class B passes the same data to the class C. Then C prints the data.
// A class A extends StatelessWidget{ int counter; A({this.counter}); @override Widget build(BuildContext context){ return new B(counter: counter); } } // B class B extends StatelessWidget{ int counter; B({this.counter}); @override Widget build(BuildContext context){ return new C(counter: counter); } } // C class C extends StatelessWidget{ int counter; C({this.counter}); @override Widget build(BuildContext context){ return Text($counter); } }
This is not a very efficient way of doing this specially if your parent-child level goes very deep. There should be a way to share the data to all the widgets. For example you might wanna have a common state that is application specific, and you want to use it wherever you want without passing the data through constructor to constructor. Much like a State management pattern. The Inherited Widget solves this problem. We will see how we can pass the data to its decendent widgets using Inherited Widget.
Create a seperate file where you will define your inherited widget. Here is an example how you can create a Inherited Widget.
// app_data.dart; import 'package:flutter/material.dart'; class AppData extends InheritedWidget{ int userid; AppData({Key key, this.userid, Widget child}) : super(key: key, child: child); @override bool updateShouldNotify(AppData old) => true; }
This class is very simple. It extends InheritedWidget
class, defines a instance member userid
, and overrides the updateShouldNotify
method. The only required parameter for Inherited Widget is child
, and we pass the child to the parent constructor.
The method updateShouldNotify
method always returns true
. We will explain about this later on.
Now you can use this class as an widget:
// class_a.dart import 'package:flutter/material.dart'; import "app_data.dart"; class ClassA extends StatelessWidget{ @override Widget build(BuildContext context){ return AppData( userid: 100, child: Center( child: ClassB(), ), ), } }
One important thing to note that we are using the Inherited widget within the ClassA
. So only those child widgets that are decendent of the ClassA
, will get access to the inherited widget AppData
. Any widget that outside of the class ClassA
cannot get access to that inherited widget. As ClassA
contains ClassB
, ClassB
will have access to the Inherited widget.
Next we will see how we can access Inherited Widget from ClassB
.
// class_b.dart import 'package:flutter/material.dart'; import "app_data.dart"; class ClassB extends StatefulWidget{ _ClassBState createState() => _ClassBState(); } class _ClassBState extends State<ClassB>{ @override Widget build(BuildContext context){ AppData appdataState = context.dependOnInheritedWidgetOfExactType<AppData>(); int userid = appdataState.userid; return Center( child: Text("$userid"), ); } }
The call to the method dependOnInheritedWidgetOfExactType
returns the nearest ancestor Inherited Widget of the type T
, in our case the T
is AppData
. So the method would return the neareset AppData
widget or instance. The statement
AppData appdataState = context.dependOnInheritedWidgetOfExactType<AppData>();
Looks very unreadable. And you have to use this long statement everywhere you want to access the widget instance. We will simplyfiy this by overriding a static method of
.
// app_data.dart; import 'package:flutter/material.dart'; class AppData extends InheritedWidget{ int userid; AppData({Key key, this.userid, Widget child}) : super(key: key, child: child); static AppData of(BuildContext context){ return context.dependOnInheritedWidgetOfExactType<AppData>(); } @override bool updateShouldNotify(AppData old) => true; }
Now you can access the Inherited widget in following way:
AppData appdataState = AppData.of(context);
This is so far the very basic of Inherited Widget. Next we will learn how everything works.
Every widgets that you are creating its either Stateful or Stateless, always have an internal private variable _inheritedWidgets
.
Map<Type, InheritedElement> _inheritedWidgets;
This private variable contains all your ancestor Inherited widgets with its corresponding type.
When you wrap your widget inside a Inherited Widget, that Inherited widget gets added to the variable _inheritedWidgets
and that variable is than passed to its child widgets.
So the code:
return AppData( userid: 100, child: Center( child: ClassB(), ), );
will add the inherited widget AppData
to the variable _inheritedWidgets
, and pass that variable to its child widget Center
and ClassB
.
When you call the method dependOnInheritedWidgetOfExactType
on the current context
, it will use the variable _inheritedWidgets
to look for the desired inherited widget. The type after the method dependOnInheritedWidgetOfExactType
specifies which type of inherited widget we want to look for. In our case we want to look for AppData
inherited widget, thats why the type is necessary after this method.
context.dependOnInheritedWidgetOfExactType<AppData>();
This method will return the specified inherited widget instance.
This method also subscribes the current calling widget to its dependencies. What does that mean? Well, every inherited widget has a private variable _dependents
. This variable stores widgets that depends on this inherited widget. In our above example, the class ClassB
calls the method dependOnInheritedWidgetOfExactType<AppData>()
, that means the widget ClassB
depends on this inherited widget class AppData
. So a reference of the widget ClassB
will get added to the variable _dependents
. That's how a inherited widget knows where it is being used. When you change the value of inherited widget, the inherited widget calls didChangeDependencies()
and build()
methods of its dependent child widgets. That's how whenever you make a change to the inherited widget, the widgets that uses the inherited widget gets rebuild automatically.
One important point to note that Inherited Widget by nature does nothing but holding the data. It doesn't have any mechanism to update the data it contains. That means you can only access the data from Inherited widget but you won't be able to change it. That is the immutable nature of the Inherited widget. But there will be a situation where you want to mutate the data and notify the dependent widgets that the data has changed so that the dependent widget rebuilds itself to update with the changes. You can directly assign values to the Inherited Widget instance like the following:
AppData appdataState = context.dependOnInheritedWidgetOfExactType<AppData>(); appdataState.userid = 100;
This approach will update the data successfully but it will not notify any dependents. There is no way to tell the inherited widget to notify its dependents. So how can we make things work?
The solution is to wrap the Inherited Widget inside a StatefulWidget. In the next lession we will cover this topic.
The Stateful widget will define some callback along with the data. When you wrap your Inherited Widget inside a Stateful widget, you will pass these data and callbacks from Stateful widgets to Inherited widgets. When you call that method using the inherited widget instance, it will mutate the data using setState()
. This will cause the Stateful widget to rebuild. When it rebuilds, it creates a brand new instance of the Inherited Widget. Here is an example:
// app_data.dart; import 'package:flutter/material.dart'; class AppDataContainer extends StatefulWidget{ Widget child; AppDataContainer({this.child}); @override _AppDataContainerState createState() => _AppDataContainerState(); } class _AppDataContainerState extends State<AppDataContainer>{ // Define your app data here -- int userid; // defines your methods here -- void assignUser(int userId){ setState((){ userid = userId; }); } @override initState(){ userid = 0; } @override Widget build(BuildContext context){ return AppData( child: widget.child, userid: userid, assignUser: assignUser ); } } class AppData extends InheritedWidget{ int userid; ValueChanged<int> assignUser; AppData({ Key key, Widget child, this.userid, this.assignUser, }) : super(key: key, child: child); static AppData of(BuildContext context){ return context.dependOnInheritedWidgetOfExactType<AppData>(); } @override bool updateShouldNotify(AppData old) => true; }
The AppData
class which extends InheritedWidget
hasn't changed much. This time we have added a new callback assignUser
as an instance member so that not only you can access the data the inherited widget contains, but you will also be able to call method on the inherited widget instance.
The stateful widget AppDataContainer
has one required parameter which is child
. This child
is passed down to the inherited widget.
In the state class you will define all your instance data and methods. These instance data and methods are passed down to the inherited widget. In the initState
method you should initialize your data with the default values. The build
method of the state class uses the inherited widget and passes the required data and methods to the inherited widget.
Now you have wrapped your inherited widget within the stateful widget. Now you can use AppDataContainer
class to wrap your widgets.
// class_a.dart import 'package:flutter/material.dart'; import "app_data.dart"; class ClassA extends StatelessWidget{ @override Widget build(BuildContext context){ return AppDataContainer( child: Center( child: ClassB(), ), ), } }
When you wrap your widgets within AppDataContainer
class you are also wrapping your widgets within inherited widget AppData
.
Now use AppData
inherited widget to get the instance and call the method to update the data.
// class_b.dart import 'package:flutter/material.dart'; import "app_data.dart"; class ClassB extends StatefulWidget{ _ClassBState createState() => _ClassBState(); } class _ClassBState extends State<ClassB>{ @override Widget build(BuildContext context){ AppData appdataState = AppData.of(context); int userid = appdataState.userid; return Center( child: InkWell( onTap: (){ appdataState.assignUser(userid + 100); }, child: SizedBox( width: double.infinity, height: double.infinity, child: Center( child: Text(userid.toString(), style: TextStyle(fontSize: 30)), ), ) ), ); } }
This is it, now tap on the box, you will see the value changed instantly. When you tap on the box, it calls the method assignUser
with a new value. This method is defined in the state class of AppStateContainer
which calls setState()
method and update the instance property userid
. This will cause the state class to call build()
method to rebuilt the state class. When the state class is rebuilt, it create a brand new instance of the inherited widget AppData
.
When a old inherited widget instance is replaced by a new instance, the framework update the private variable _inheritedWidgets
with the mapped key which is in our case AppData
. Then it calls the didChangeDependencies()
and build()
method of each dependent that is assigned to inherited widget AppData
.
Thats it, you can have as many data and methods as you want in your state class and if you pass then down to inherited widget, you can access then with the inherited widget instance. This way you can change the data anytime and your UI will always react to the changes instantly.
But there is one limitation. When you change the data of the state class, it will always create a brand new instance of the inherited widget which will cause the dependent widgets to rebuilt to update themselves. So even if you change something small, all the dependent will be rebuilt. If your page contains too many dependent widgets, for each changes to the data, all dependent widgets will be rebuilt. Sometimes you want a particular dependent to avoid unncecessary rebuilding when you know that this particular dependent widget doesn't rely on the changes. You will want to rebuilt only those dependent widgets that depends on a particular data.
There should be a way to filter the dependent widgets so that you can decide if a particular dependent widget should respond to the changes. Unfortunately there is no way you can do this with the help of a Inherited Widget.
This limitation can be removed by using InheritedModel
widget. InheritedModel
widget is a subclass of InheritedWidget
class. InheritedModel
works the same way as InheritedWidget
but it also has a additional functionality to filter out the dependents before it calls didChangeDependencies()
method. We will learn how to do that with InheritedModel
widget in the next lesson.
The implementation of the InheritedModel is almost same as InheritedWidget. Here is the implementation of the InheritedModel that users the previous example.
import 'package:flutter/material.dart'; enum AspectsList{ USERID_CHANGED, COUNTER_CHANGED, } class AppDataContainer extends StatefulWidget{ Widget child; AppDataContainer({this.child}); @override _AppDataContainerState createState() => _AppDataContainerState(); } class _AppDataContainerState extends State<AppDataContainer>{ // Define your app data here -- int userid; int counter; // defines your methods here -- void assignUser(int userId){ setState((){ userid = userId; }); } void assignCounter(int value){ setState((){ counter = value; }); } @override initState(){ userid = 0; counter = 0; } @override Widget build(BuildContext context){ return AppData( child: widget.child, userid: userid, counter: counter, assignUser: assignUser, assignCounter: assignCounter, ); } } class AppData extends InheritedModel<AspectsList>{ int userid; int counter; ValueChanged<int> assignUser; ValueChanged<int> assignCounter; AppData({ Key key, Widget child, this.userid, this.counter, this.assignUser, this.assignCounter, }) : super(key: key, child: child); static AppData of(BuildContext context, {AspectsList aspect}){ return InheritedModel.inheritFrom<AppData>(context, aspect: aspect); } @override bool updateShouldNotify(AppData old) => true; @override updateShouldNotifyDependent(AppData old, Set<AspectsList> aspect){ bool changed = false; if(aspect.contains(AspectsList.USERID_CHANGED)){ changed = changed || old.userid != userid; } if(aspect.contains(AspectsList.COUNTER_CHANGED)){ changed = changed || old.counter != counter; } return changed; } }
First consider the AppData
class that extends InheritedModel
class. The implementation is almost same as InheritedWidget except few things:
Specify the type of the aspect that you are going to use after the class name InheritedModel
. Here we have used a enum AspectsList
type as the type of the aspect.
class AppData extends InheritedModel<AspectsList>
But what is aspect? You can think of it as a key or keyword. The aspect specifies what kind of change the data of the InheritedModel instance took place. Each aspect should be bound to each data member. For userid
data member there is a USERID_CHANGED
constant and for counter
data member there is a COUNTER_CHANGED
constant defined in the enum AspectsList
. Leter we will use these aspects to determine if a particular data member has changed or not.
Each dependent of a InheritedModel widget is bound to one or more aspect. If your dependent widget depends on more than one data member, you should specify each aspect for each data member. The above program is currently configured to accept only one aspect. But later we will see how we can configure our example to make it accept multiple aspects.
Everytime you replace an old InheritedModel instance with a new instance, flutter calls a method updateShouldNotifyDependent
for each dependent widget to determine if the data members that the dependent widget depends on has changed or not. The method updateShouldNotifyDependent
is called with the aspects that the dependent widget is bound to.
InheritedModel
uses aspects to check if a particular dependent widget should be rebuild. You can also use String
, or int
, but in our example we have defined it as a enum
type. So that the list of aspects can be seen in one place and will be easier to maintain and it is much more redable. Leter we will see how to bind aspects with the dependent widget.
Coming next, another difference is that we are using InheritedModel.inheritFrom<T>()
method to look for nearest InheritedModel
widget instance instead of dependOnInheritedWidgetOfExactType()
method. This is important for InheritedModel
widget. You also need to specify the type of the InheritedModel
that we are going to search for. In our case we are going to look for AppData
InheritedModel widget, that's why there is type mentioned after the method name.
InheritedModel.inheritFrom<AppData>(context, aspect: aspect);
This method accepts two argument, the first argument is positional argument which is conext
, and second argumen is a named argument aspect
. The argument aspect
is optional, that is why in the parameter list of of
method, we have wrapped it within the curly bracket which makes it named optional parameter.
static AppData of(BuildContext context, {AspectsList aspect}){ return InheritedModel.inheritFrom<AppData>(context, aspect: aspect); }
Lastly, another notable difference is that we also have to override the method updateShouldNotifyDependent
. This method is responsible for checking if a dependent should be updated. This method has two parameter, the first parameter is the old instance of InheritedModel widget, and second parameter is a Set of aspects. As our aspect type is AspectsList
, thats why our second parameter is typed as Set<AspectsList>
.
updateShouldNotifyDependent(AppData old, Set<AspectsList> aspect){
That is all about implementing the InheritedModel widget, the rest of the code is same.
Using the InheritedModel is same as InheritedWidget. The class ClassA
is same as before:
// class_a.dart import 'package:flutter/material.dart'; import "app_data.dart"; class ClassA extends StatelessWidget{ @override Widget build(BuildContext context){ return AppDataContainer( child: Center( child: ClassB(), ), ), } }
You can access the InheritedModel widget the same way you have done it with InheritedWidget, excepts you need to pass the aspect
as optional argument. Here is a different example of ClassB
:
import 'package:flutter/material.dart'; import "app_data.dart"; class ClassB extends StatefulWidget{ @override _ClassBState createState()=> _ClassBState(); } class _ClassBState extends State<ClassB>{ @override Widget build(BuildContext context){ return Center( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ UserBox(), SizedBox(width: 100), CounterBox(), ] ), ); } } class UserBox extends StatefulWidget{ @override _UserBoxState createState() => _UserBoxState(); } class _UserBoxState extends State<UserBox>{ @override Widget build(BuildContext context){ print("User"); AppData model = AppData.of(context, aspect: AspectsList.USERID_CHANGED); int userId = model.userid; return DecoratedBox( decoration: BoxDecoration(color: Colors.red), child: InkWell( child: SizedBox( width: 100, height: 100, child: Center( child: Text(userId.toString(), style: TextStyle(fontSize: 30, color: Colors.white)), ), ), onTap: (){ model.assignUser(userId + 1); } ), ); } } class CounterBox extends StatefulWidget{ @override _CounterBoxState createState() => _CounterBoxState(); } class _CounterBoxState extends State<CounterBox>{ @override Widget build(BuildContext context){ print("Counter"); AppData model = AppData.of(context, aspect: AspectsList.COUNTER_CHANGED); int counter = model.counter; return DecoratedBox( decoration: BoxDecoration(color: Colors.pink), child: InkWell( child: SizedBox( width: 100, height: 100, child: Center( child: Text(counter.toString(), style: TextStyle(fontSize: 30, color: Colors.white)), ), ), onTap: (){ model.assignCounter(counter + 1); } ), ); } }
Notice how we are accessing the InheritedModel using the following statement:
AppData.of(context, aspect: AspectsList.COUNTER_CHANGED);
It is quite same as InheritedWidget but only difference is that we are also passing aspect. When you call this method, the widget is going to be stored as dependent widget of the InheritedModel AppData
in the _dependent
variable. It also stores the passed aspect along with the dependent widget in the _dependent
variable. This way the InheritedModel widget instance tracks its dependent widget. When you create anew instance of the same type of InheritedModel and replace it in place of old one, the flutter calls updateShouldNotify
method of the InheritedModel widget, if that returns false
, the dependents widgets will not be notified at all. If this method returns true
, then flutter calls the method updateShouldNotifyDependent
for each and every dependent widget seperately and passes two argument to that method, the first parameter is the old instance of the InheritedModel
widget, which is in our case AppData
instance, and second parameter is the dependent widget's corresponding aspect
. The aspect is passed as a Set.
updateShouldNotifyDependent(AppData old, Set<AspectsList> aspect){ bool changed = false; if(aspect.contains(AspectsList.USERID_CHANGED)){ changed = changed || old.userid != userid; } if(aspect.contains(AspectsList.COUNTER_CHANGED)){ changed = changed || old.counter != counter; } return changed; }
In that method we are checking if the dependent widgets aspect matches any of the contant of the AspectsList enum, if it does, then we are checking if a particular instance field has changed or not, and according to it if this method returns true
, the dependent widget will rebuilt and if returns false
, it will skip the rebuilding.
If you do not provide the aspect like the following statement, the method updateShouldNotifyDependent
will not be called for that dependent widget. Because if you don't supply aspect, its aspect is null
, in this case the dependent widget will be rebuilt unconditionally.
AppData model = AppData.of(context);
So far we have discussed the InheritedModel which is configured to accept only one aspect. You cannot pass multiple aspects. Even though the doc says that a dependent widget may have multiple aspects but it is not clear how to do that. But the following solution works anyway.
First change the type of the aspect in the InheritedModel class declaration:
class AppData extends InheritedModel<Set<AspectsList>>{ }
Now our model will accept a set of AspectsList
enum constant. Next update the of method. In the parameter list, change the type from AspectsList
to Set<AspectsList>
static AppData of(BuildContext context, {Set<AspectsList> aspect}){ return InheritedModel.inheritFrom<AppData>(context, aspect: aspect); }
Now you can pass a set to the of
method as the second argument. Next, update the updateShouldNotifyDependent
method.
updateShouldNotifyDependent(AppData old, Set<Set<AspectsList>> aspect){ bool changed = false; Set<AspectsList> widgetAspects = aspect.elementAt(0); if(widgetAspects.contains(AspectsList.USERID_CHANGED)){ changed = changed || old.userid != userid; } if(widgetAspects.contains(AspectsList.COUNTER_CHANGED)){ changed = changed || old.counter != counter; } return changed; }
Now you should always pass a set as the aspect. For example, if your widget depends on both userid
and counter
data field of the model, then you should use two aspects, USERID_CHANGED
for userid
, and COUNTER_CHANGED
for counter
member. If any of these member changes updateShouldNotifyDependent
will return true
for that widget.
AppData model = AppData.of(context, aspect: {AspectsList.USERID_CHANGED, AspectsList.COUNTER_CHANGED});
This way you can specify that your dependent widget depends on more than one aspect. If any of the aspects matches in updateShouldNotifyDependent
method, it checks if the corresponding data member has changed or not.
This is all about InheritedWidget and InheritedModel.