Sample code for this article
Data sharing InheritedWidget
InheritedWidget is an important functional component of Flutter that provides a way for data to be passed from top to bottom in the widget tree. For example, if we share data with an InheritedWidget in the root Widget, we can retrieve the shared data in any child Widget.
This feature is useful in scenarios where data is shared in the widget tree. For example, the Fluter SDK uses this widget to share application theme and Locale information.
didChangeDependencies
This callback is a State object that is invoked by the Flutter Framework when a dependency changes. This dependency is whether the InheritedWidget uses data from the parent widget.
If used, the component relies on the InheritedWidget, if not used, there is no dependency.
This mechanism enables inheritedWidgets that a child component depends on to update themselves when changes occur, such as themes, and the didChangeDependencies method of the dependent child widget is called when changes occur
Here’s a chestnut:
class ShareDataWidget extends InheritedWidget {
// Data to be shared
final int data;
ShareDataWidget({@required this.data, Widget child}) : super(child: child);
// Define a convenient method for widgets in a subtree to get shared data
static ShareDataWidget of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}
///This callback determines whether to notify data-dependent widgets in the subtree when data changes
@override
bool updateShouldNotify(covariant ShareDataWidget oldWidget) {
// Returns true: The didChangeDependencies of widgets in the subtree that depend on the current widget are called
return oldWidget.data != data;
}
}
Copy the code
This defines a shared ShareDataWidget that InheritedWidget and holds a data attribute, which is the data to be shared
class TestShareWidget extends StatefulWidget {
@override
_TestShareWidgetState createState() => _TestShareWidgetState();
}
class _TestShareWidgetState extends State<TestShareWidget> {
@override
Widget build(BuildContext context) {
return Text(ShareDataWidget.of(context).data.toString());
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('Change'); }}Copy the code
A child component is implemented above that uses the ShareDataWidget data in the build method and prints the log in the callback
Finally, create a button that increases the value of ShareDataWidget by clicking it once
class TestInheritedWidget extends StatefulWidget {
@override
_TestInheritedWidgetState createState() => _TestInheritedWidgetState();
}
class _TestInheritedWidgetState extends State<TestInheritedWidget> {
int count = 0;
@override
Widget build(BuildContext context) {
return Center(
child: ShareDataWidget(
data: count,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20.0),
child: TestShareWidget(),
),
RaisedButton(
child: Text("Increment"),
// With each click, count increases, then build again, ShareDataWidget will be updatedonPressed: () => setState(() => ++count), ) ], ), ), ); }}Copy the code
The effect is as follows:
As you can see, the did of the child component after the dependency changes… The method will be called. Note that if TestShareWidget’s build method does not use ShareDataWidget’s data, its DID… Method will not be called because it does not rely on ShareDataWidget;
For example:
class _TestShareWidgetState extends State<TestShareWidget> {
@override
Widget build(BuildContext context) {
// return Text(ShareDataWidget.of(context).data.toString());
return Text("test");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('Change'); }}Copy the code
Comment out the buid code that depends on ShareDataWidget, and return a fixed Text. The didChangeDependencies method is not called because data changes
Because it is reasonable and performance-friendly to update only the widgets with that data when the data changes
Should be in did… What do you do in the method
In general, child widgets rarely revisit this method because the Build method is also called after a dependency has changed. However, if you need to do expensive operations when dependencies change, such as network requests, the best way to do this is to avoid doing these expensive operations every time you build
Understand the InheritedWidget method in depth
If we only want to rely on data and don’t want to execute the didChangeDependencies method when a dependency changes, here’s how:
// Define a convenient method for widgets in a subtree to get shared data
static ShareDataWidget of(BuildContext context) {
// return context.dependOnInheritedWidgetOfExactType();
return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>().widget;
}
Copy the code
Will get ShareDataWidget ways to the context. GetElementForInheritedWidgetOfExactType () the widget
So what is the difference between these two ways? Let’s take a look at the source code:
@override
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null: _inheritedWidgets! [T];return ancestor;
}
Copy the code
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement? ancestor = _inheritedWidgets == null ? null: _inheritedWidgets! [T];// Compared to the above code, the extra part
if(ancestor ! =null) {
assert(ancestor is InheritedElement);
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
Copy the code
As you can see, dependOnInheritedWidgetOfExactType than getElementForInheritedWidgetOfExactType dependOnInheritedElement method, His source code is as follows:
@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) {
assert(ancestor ! =null); _dependencies ?? = HashSet<InheritedElement>(); _dependencies! .add(ancestor); ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
Copy the code
As you can see, dependOnInheritedElement method is mainly registered the dependencies, so, in the call getElementForInheritedWidgetOfExactType, InheritedWidget and dependant descendant components are registered, and when the InheritedWidget changes, you can update the build and didChangeDependencies methods of the dependant descendant component.
Because getElementForInheritedWidgetOfExactType not depend on registration, so when InheritedElement changes, the children will not update the corresponding widget
Note: On the case, the changes of methods to getElementForInheritedWidgetOfExactType after _TestShareWidgetState didChangeDependencies method does not be invoked, But the build method is called anyway;
This is because after the button is clicked, the setState method of _TestInheritedWidgetState is called, at which point the page is rebuilt, causing TestShareWidget() to be rebuilt, so his build is executed
In this case, components that rely on ShareDataWidget will be re-built whenever the setState method _TestInheritedWidgetState is called. This is unnecessary. What can be avoided?
A simple way to do this is to cache the Widget tree by encapsulating a StatefulWidget so that it can be executed in a build;
Share providers across component states
The general principles of state management in Flutter are:
- If the component is private, the component manages its own state
- If shared across components, state is managed by a common parent component
There are many ways to manage shared state across components, such as using the global practice bus EventBus, which is an implementation of the observer pattern, through which state synchronization can be achieved across components: State holder: performs state updates, publishes state and uses; State users (observers) listen for state changes to perform some operations;
However, implementing cross-components through the observer pattern has some obvious drawbacks:
-
Events must be explicitly defined, making it difficult to manage
-
Subscribers must register explicitly for state-change callbacks and must also manually unbind callbacks at component destruction to avoid memory leaks
Is there a better way to manage this? Yes, use inheritedWidgets, which inherently bind inheritedwidgets to descendant components that depend on them, and then automatically rely on them when data changes! Using this feature, we can save the required state across components in the InheritedWidget and then reference the InheritedWidget in the child component. The Flutter community’s famous Provider package is a solution for sharing state across components based on this idea. Let’s take a closer look at how providers work.
Provider
We implement a minimal Provider step by step based on the InheritedWidget implementation we learned above
Define an InheritedWidget that needs to save data
///A generic InheritedProvider holds any state that needs to be shared across components
class InheritedProvider<T> extends InheritedWidget {
///Shared state uses generics
final T data;
InheritedProvider({@required this.data, Widget child})
: super(child: child);
@override
bool updateShouldNotify(covariant InheritedWidget oldWidget) {
///Returns true, indicating that the descendants' didChangeDependencies are called each time
return true; }}Copy the code
Because the specific business data types are unpredictable, generics are used for generality
Now that you have a place to save the data, all you need to do is rebuild the InheritedProvider when the data changes, so there are two problems:
- How to notify data changes?
- Who will rebuild
InheritedProvider
?
The first problem is actually easy to solve. We can use EventBus for notifications, but to get closer to Flutter development, we use the ChangeNotifier class provided with the Flutter SDK, which is derived from Listenable. Also implements a Flutter style subscriber pattern, defined roughly as follows:
class ChangeNotifier implements Listenable {
List listeners=[];
@override
void addListener(VoidCallback listener) {
// Add a listener
listeners.add(listener);
}
@override
void removeListener(VoidCallback listener) {
// Remove the listener
listeners.remove(listener);
}
void notifyListeners() {
// Notify all listeners and trigger listener callbacklisteners.forEach((item)=>item()); }...// omit extraneous code
}
Copy the code
Listeners can be added or removed using add and remove, and notifyListeners can trigger callbacks on all listeners
We then place the state we need to share in a Model class and inherit it from The ChangeNotifier, so that when the shared state changes, we only need to call notifyListeners, and the subscribers rebuild the InheritedProvider. This is also the answer to the second question. The subscription class is implemented as follows:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget {
final Widget child;
final T data;
ChangeNotifierProvider({Key key, this.child, this.data});
@override
_ChangeNotifierProviderState<T> createState() =>
_ChangeNotifierProviderState<T>();
///Define a convenient method for widgets in a subtree to get shared data
static T of<T>(BuildContext context) {
final provider =
context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>();
returnprovider.data; }}class _ChangeNotifierProviderState<T extends ChangeNotifier>
extends State<ChangeNotifierProvider<T>> {
void update() {
setState(() {});
}
@override
void initState() {
// Add a listener to model
widget.data.addListener(update);
super.initState();
}
@override
void dispose() {
// Remove model listeners
super.dispose();
}
@override
void didUpdateWidget(covariant ChangeNotifierProvider<T> oldWidget) {
// When the Provider updates, if the old data does not ==, unbind the old data listener and add the new data listener
if(widget.data ! = oldWidget.data) { oldWidget.data.removeListener(update); widget.data.addListener(update); }super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
returnInheritedProvider<T>( data: widget.data, child: widget.child, ); }}Copy the code
The ChangeNotifierProvider inherits StatefulWidget and defines a static of method for subclasses to easily get the shared state saved in the InheritedProvider of the Widget tree
_ChangeNotifierProviderState class main role is to monitor share state changes to build the Widget tree. Note that when the setState() method is called in this class, widget.child is always the same, and InheriedProvider’s child is always referencing the same child widget, so widget.child is not rebuilt. This is equivalent to caching the child. Of course, if the ChangeNotifierProvider abs Widget is rebuilt, the child passed in May change
Now that we have all the utility classes we need, let’s look at an example of how to use the above class
Shopping cart example
///The Item class, used to represent information about an Item
class Item {
final price;
int count;
Item(this.price, this.count);
}
class CarMode extends ChangeNotifier {
// The user saves the list of items in the shopping cart
final List<Item> _items = [];
// Do not modify item information in shopping cart
UnmodifiableListView get items => UnmodifiableListView(_items);
// Total price of items in shopping cart
double get totalPrice =>
_items.fold(0, (value, item) => value + item.count * item.price);
void add(Item item) {
_items.add(item);
// Notify the listener, rebuild the InheritedProvider, and update the statusnotifyListeners(); }}class ProviderTest extends StatefulWidget {
@override
_ProviderTestState createState() => _ProviderTestState();
}
class _ProviderTestState extends State<ProviderTest> {
@override
Widget build(BuildContext context) {
return Center(
child: ChangeNotifierProvider(
data: CarMode(),
child: Builder(
builder: (context) {
return Column(
children: [
Builder(builder: (context) {
var cart = ChangeNotifierProvider.of<CarMode>(context);
return Text("The total price:${cart.totalPrice}");
}),
Builder(
builder: (context) {
return RaisedButton(
child: Text("Add goods"),
onPressed: () {
ChangeNotifierProvider.of<CarMode>(context)
.add(Item(20.1)); }); },)],); },),),); }}Copy the code
Item class: A system of information used to hold goods
The CartMode class: the class that holds the data above the cart, that is, the Model class that needs to be shared across components
ProviderTest: The final page built
Each time you click add, the total price will increase by 20. Although this example is relatively simple and only updates one state in the same routing page, if it is a real shopping cart, its shopping cart data will normally be shared within the app, such as across routes. Place the ChangeNotifierProvider at the root of the Widget tree for the entire app so that the shopping cart data can be shared across the app
The schematic diagram of the Provider is as follows:
The ChangeNotifierProvider (subscriber) is automatically notified when the Model changes, and the InheritedWidget is rebuilt internally by ChangeNotifierProvider, The InheritedWidget’s descendant widgets that rely on that InheritedWidget are updated
We can see that using providers brings the following benefits:
1. Our business code pays more attention to data. If we only need to update Model, the UI will be updated automatically, instead of manually calling setState to explicitly update the page after the state changes
2. The messaging of data changes is masked, so we don’t have to manually handle publishing and subscribing to change events. All of this is encapsulated in the Provider, which saves us a lot of work
3. In large and complex applications, especially when there are many states that need to be shared globally, using Provider will greatly simplify our code logic, reduce error probability and improve development efficiency
To optimize the
The ChangeNotifierProvider implemented above has two obvious drawbacks: code organization issues and performance issues, which we’ll look at below
Code organization issues
Builder(builder: (context) {
var cart = ChangeNotifierProvider.of<CarMode>(context);
return Text("The total price:${cart.totalPrice}");
}),
Copy the code
There are two things you can optimize this code for
1. The ChangenotifierProvider that needs to be displayed is called. When the APP relies on CartMode internally, such code will be very heavy
2. Semantic ambiguity. Since ChangenotifierProvider is a subscriber, widgets that rely on CarMode are naturally subscribers, i.e. consumers of the state. If you build with a Builder, the semantics are not very clear, but if you use a more semantic Widget like Consumer, the language of the final code is clear, and when you see Consumer, you know that it is some cross-component or global state.
To optimize this problem, we can encapsulate a Consumer Widget like this:
class Consumer<T> extends StatelessWidget {
final Widget child;
final Widget Function(BuildContext context, T value) builder;
Consumer({Key key, @required this.builder, this.child});
@override
Widget build(BuildContext context) {
return builder(context, ChangeNotifierProvider.of<T>(context)); // Get the model automatically}}Copy the code
Cusumer implementation is very simple, it by specifying the template parameters, then the internal automatic call ChangeNotifierProvider. The corresponding Mode of access to, the name and Consumer itself also has precise semantics (Consumer), now the above code can be optimized as follows:
Consumer<CarMode>(
builder: (context, cart) => Text("The total price:${cart.totalPrice}"),),Copy the code
Isn’t it elegant?
Performance issues
The code above also has a performance problem where buttons are added:
Builder(
builder: (context) {
return RaisedButton(
child: Text("Add goods"),
onPressed: () {
ChangeNotifierProvider.of<CarMode>(context)
.add(Item(20.1)); }); },)Copy the code
After clicking the Add item button, the Text showing the summary is expected because the total cart price will change.
The Add Item button itself is unchanged, so it should not be rebuilt, but the runtime finds that the button is rebuilt every time it is clicked. And why is that, This is because RadisedButton build invokes the ChangeNotifierProvider. Of (), that is dependent on the Widget InheritedWidget in the tree (i.e., InheritedProvider) The Widget,
Therefore, when the CartMode changes after the goods are added, the sub-tree is notified, so that the InheritedProvider will be updated, and the Wdiget dependent on it will be rebuilt.
Problem identified, so how do you avoid this unnecessary refactoring?
Since the problem is that the button has built a dependency with the InheritedWidget, all we need to do is break that dependency. How to do that is explained at the top: Call dpendOnInheritedWidgetOfExactType and getElementForInheritedWidgetOfExactType difference is that the former will depend on registration, while the latter doesn’t, All you need to do is change the implementation of ChangenotiferProvider.of to something like this:
///Listen: Whether to establish a relationship
static T of<T>(BuildContext context, {bool listen = true{})final provider = listen
? context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>()
: context
.getElementForInheritedWidgetOfExactType<InheritedProvider<T>>()
?.widget as InheritedProvider<T>;
return provider.data;
}
Copy the code
If you run it again, the button will not be rebuilt, and the total will be updated.
So far, we have implemented a mini-version of Provider, which has the core functions of the Provider package on the Pub. However, because our functions are not comprehensive, we only implemented a listening ChangeNotiferProvider, and did not realize data sharing. In addition, There are bound values that our implementation does not take into account, such as how to ensure that Mode is always a singleton when the Widget tree is rebuilt. Therefore, it is recommended to use Provider Package in the project. This article only helps you understand the basic principle of Provider Package
This article refers to Fluuter actual combat books