Flutter data is transmitted in two ways. One is to pass states up and down in the direction of numbers. The other is to pass state values from the bottom up.
I’m passing state down the tree
Pass state from top to subtrees and nodes in the direction of the Widgets Tree.
InheritedWidget & ValueNotifier
InheritedWidget
This familiar and unfamiliar class helps us pass information down the tree in a Flutter. We often get Theme and MediaQuery in this way, by BuildContext
// Get the height of the status bar
var statusBarHeight = MediaQuery.of(context).padding.top;
// copy and merge new themes
var copyTheme =Theme.of(context).copyWith(primaryColor: Colors.blue);
Copy the code
When you see a static method of of, the first reaction is to build a new class from the context. And then from that class, call the method that gets the state. (Familiar to Android developers, like Picasso, Glide). But is it?
MediaQuery
throughcontext.inheritFromWidgetOfExactType
static MediaQueryData of(BuildContext context, { bool nullOk: false{})assert(context ! =null);
assert(nullOk ! =null);
final MediaQuery query = context.inheritFromWidgetOfExactType(MediaQuery);
if(query ! =null)
return query.data;
if (nullOk)
return null;
throw new FlutterError(
'MediaQuery.of() called with a context that does not contain a MediaQuery.\n'
'No MediaQuery ancestor could be found starting from the context that was passed '
'to MediaQuery.of(). This can happen because you do not have a WidgetsApp or '
'MaterialApp widget (those widgets introduce a MediaQuery), or it can happen '
'if the context you use comes from a widget above those widgets.\n'
'The context used was:\n'
' $context'
);
}
Copy the code
- First of all, you can see through this method
context.inheritFromWidgetOfExactType
To check theMediaQuery
.MediaQuery
It is we who existBuildContext
Property in. - Secondly, you can see that
MediaQuery
Stored in theBuildContext
The position of theWidgetsApp
Because in factMaterialApp
It is also returned.)
MediaQuery state preservation principle
-
Inheritance InheritedWidget
-
through
build
Method returns
-
Build method in _MaterialAppState of the MaterialApp
-
Build method in _WidgetsAppState of WidgetsApp
- To obtainAnd finally, the code you see at the top, passes
context.inheritFromWidgetOfExactType
In order to get. And then anywhere in the subtree, you can get it this way.
Define an AppState
Knowing how MediaQuery is stored, we can implement our own state management so that state values can be retrieved synchronously in child components.
0. Define an AppState
//0. Define a variable to store class AppState {bool isLoading; AppState({this.isLoading =true});
factory AppState.loading() => AppState(isLoading: true);
@override
String toString() {
return 'AppState{isLoading: $isLoading}'; }}Copy the code
1. InheritedWidget inheritance
//1. Imitate MediaQuery. Simply let this hold the data we want to save
class _InheritedStateContainer extends InheritedWidget {
final AppState data;
// We know that the InheritedWidget is always wrapped as a layer, so it must have a child
_InheritedStateContainer(
{Key key, @required this.data, @required Widget child})
: super(key: key, child: child);
// Refer to MediaQuery. This method is usually implemented this way. If the new value is not equal to the old value, notify is required
@override
boolupdateShouldNotify(_InheritedStateContainer oldWidget) => data ! = oldWidget.data; }Copy the code
2. Create the outer Widget
Create the outer Widget and provide the static method of to get our AppState
/* 1. From the MediaQuery parodies, we know that we need a StatefulWidget as an outer component to build our InheritateWidget */
class AppStateContainer extends StatefulWidget {
// This state is the state we need
final AppState state;
// This child is required to display our normal controls
final Widget child;
AppStateContainer({this.state, @required this.child});
//4. Mimic MediaQuery and provide an of method to get our State.
static AppState of(BuildContext context) {
/ / this method, called the context. InheritFromWidgetOfExactType
return (context.inheritFromWidgetOfExactType(_InheritedStateContainer)
as _InheritedStateContainer)
.data;
}
@override
_AppStateContainerState createState() => _AppStateContainerState();
}
class _AppStateContainerState extends State<AppStateContainer> {
Return our InheritedWidget in the build method
// AppStateContainer->_InheritedStateContainer-> real App
@override
Widget build(BuildContext context) {
return_InheritedStateContainer( data: widget.state, child: widget.child, ); }}Copy the code
Use 3.
- It’s included in the outermost layer
class MyInheritedApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
// Because it is AppState, its scope is full life cycle, so it can be wrapped directly in the outermost layer
return AppStateContainer(
// Initialize a loading
state: AppState.loading(),
child: new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: 'Flutter Demo Home Page'),),); }}Copy the code
- In any position you want, use. It’s recommended in the documentation
didChangeDependencies
Query it in. So we also
class _MyHomePageState extends State<MyHomePage> {
_MyHomePageState() {}
AppState appState;
// In the didChangeDependencies method, you can check the corresponding state
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies');
if(appState==null){ appState= AppStateContainer.of(context); }}@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
// Display a loading or normal graph according to isLoading
child: appState.isLoading
? CircularProgressIndicator()
: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ${appState.isLoading}',
),
],
),
),
floatingActionButton: new Builder(builder: (context) {
return new FloatingActionButton(
onPressed: () {
// Click the button to switch
// Because the state is global, changes in other pages will also cause changes hereappState.isLoading = ! appState.isLoading;//setState triggers a page refresh
setState(() {});
},
tooltip: 'Increment',
child: newIcon(Icons.swap_horiz), ); })); }}Copy the code
Effect 1- Current page
Click the button to change the status.
4. Change AppState on a separate page
Since the above code is within a page, we need to determine whether the global state is consistent. So let’s change the code again, click push to create a new page, change the state of appState inside the new page, and see if the page changes. The code is modified as follows:
// Modify the click event of floatingButton
floatingActionButton: new Builder(builder: (context) {
return new FloatingActionButton(
onPressed: () {
//push a new page
Navigator.of(context).push(
new MaterialPageRoute<Null>(builder: (BuildContext context) {
return MyHomePage(
title: 'Second State Change Page');
}));
// Comment out the original code
// appState.isLoading = ! appState.isLoading;
// setState(() {});
},
tooltip: 'Increment',
child: new Icon(Icons.swap_horiz),
);
})
Copy the code
- The addition of
MyHomePage
Basically the same code as above. Let him do the sameappState
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void_changeState() { setState(() { state.isLoading = ! state.isLoading; }); } AppState state;@override
void didChangeDependencies() {
super.didChangeDependencies();
if(state ==null){ state = AppStateContainer.of(context); }}@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ${state.isLoading}',
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _changeState,
tooltip: 'ChangeState',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.); }}Copy the code
Effect 2- Change status in another page
Modify the state of AppState on the push page and go back to the original page to see if the state has changed.
Summary and thinking
By analyzing MediaQuery, we learned about the use of inheritedWidgets and familiarized ourselves with the process of overall state control through operations such as custom AppState. We can continue to think about the following questions
-
Why does AppState stay in state for the entire App cycle? Because we’ve wrapped it in the outermost layer. Thinking of this, each page may also have its own state, and maintaining the state of the page can be wrapped around the outermost layer of the page hierarchy, so that it becomes the PageScope state.
-
When we change state and close the page, we always get the latest state when we open the page because of the didChangeDependencies method and the build method. So our page can sync state successfully. So if it’s like EventBus, where we push a state, and we have to do a time-consuming operation, and then the change happens, can we listen for it and handle it?
ValueNotifier
Inherit to ChangeNotifier. You can register listening events. A listener is sent when the value changes.
/// A [ChangeNotifier] that holds a single value.
///
/// When [value] is replaced, this class notifies its listeners.
class ValueNotifier<T> extends ChangeNotifier implements ValueListenable<T> {
/// Creates a [ChangeNotifier] that wraps this value.
ValueNotifier(this._value);
/// The current value stored in this notifier.
///
/// When the value is replaced, this class notifies its listeners.
@override
T get value => _value;
T _value;
set value(T newValue) {
if (_value == newValue)
return;
_value = newValue;
notifyListeners();
}
@override
String toString() => '${describeIdentity(this)}($value)';
}
Copy the code
Notice that changes to the value of the set method are notifyListeners
Modify the code
AppState Adds a member
// Define a variable to store class AppState {//... Ignore duplicate code. ValueNotifier<bool> canListenLoading = ValueNotifierfalse);
}
Copy the code
Add listening to _MyHomeInheritedPageState
class _MyHomeInheritedPageState extends State<MyInheritedHomePage> {
/ /... Ignore duplicate code. Adding member variables
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies');
if (appState == null) {
print('state == null');
appState = AppStateContainer.of(context);
// Add a listening event hereappState.canListenLoading.addListener(listener); }}@override
void dispose() {
print('dispose');
if(appState ! =null) {
// Remove the listening event here
appState.canListenLoading.removeListener(listener);
}
super.dispose();
}
@override
void initState() {
print('initState');
// Initialize the listener callback. So what I'm going to do is I'm going to change result to "From delay" after 5s.
listener = () {
Future.delayed(Duration(seconds: 5)).then((value) {
result = "From delay";
setState(() {});
});
};
super.initState();
}
// Add member variables. The result argument and listener callback
String result = "";
VoidCallback listener;
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: appState.isLoading
? CircularProgressIndicator()
: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ${appState.isLoading}',),// Add the result to the screen
new Text(
'${result}'[, [, [, [,/ /... Ignore duplicate code}}Copy the code
The results
It worked as we expected.
- The display opens a new page.
- Change in the new page
canListenLoading
The value of. This triggers the listener event that was registered for the previous page (value changed after 4s). - Then we went back and waited and did find that the data had changed
This feels like an eventBus-like feature
summary
This article mainly describes how to implement state management and message passing using the framework of Flutter itself.
- through
InheritedWidget
To save the state - through
context.inheritFromWidgetOfExactType
To get the property - use
ValueNotifer
To implement property listening.
We can make a summary of top-down data passing and state management
-
The Key stores the state of the Widget. You can store the state by giving the Key to the Widget, and retrieve the state by using the Key. For example, ObjectKey can mark a unique Key in the list to hold the state and let the animation recognize it. GlobalKey, on the other hand, saves a state that is available elsewhere.
-
InheritedWidget can hold a state that is accessed by its subtrees. This allows the tree itself not to pass in the field directly (thus avoiding the need to pass state down layer by layer in the case of multiple widgets) and to synchronize state between different widgets
-
ChangeNofier inherits this class so that we can implement the observer mode in Flutter to observe property changes.
Alternatively, we can do this through third-party libraries such as Redux and ScopeModel Rx. But it’s based on the same principle that’s up there.
Pass the distribution status value from the bottom up
Notification
We know that we can listen for the ScrollNotification page by NotificationListener. Flutter does this by publishing data from child to parent BuildContext. Let’s simply implement one of our own.
- code
//0. Define a Notification.
class MyNotification extends Notification {}
class _MyHomePageState extends State<MyHomePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
}
@override
Widget build(BuildContext context) {
//2. The Scaffold listens for events at the Scaffold level. Create 'NotificationListener' and get our event in 'onNotification'.
return NotificationListener(
onNotification: (event) {
if (event is MyNotification) {
print("event= Scaffold MyNotification");
}
},
child: new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
//3. Here you need to listen for events, which need to be sent in the body's own 'BuildContext' !!!!
body: new NotificationListener<MyNotification>(
onNotification: (event) {
// Cannot accept events because 'context' is different
print("body event=" + event.toString());
},
child: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'appState.isLoading = ',),new Text(
'appState.canListenLoading.value',
),
],
),
)),
floatingActionButton: Builder(builder: (context) {
return FloatingActionButton(
onPressed: () {
//1. Create events and send them to the corresponding 'BuildContext'. Note that 'context' is the 'BuildContext' of 'Scaffold'
new MyNotification().dispatch(context);
},
tooltip: 'ChangeState',
child: newIcon(Icons.add), ); }))); }}Copy the code
- The results
summary
We can implement data passing by using Notification’s inheriting class and publishing it into the corresponding BuildContext.
conclusion
Through the introduction of Flutter data transfer here, we can roughly build our own data flow structure of Flutter App. The architectural design of the interface similar to idle fish.
-
From the top down: The inheritedWidgets of different scopes are customized to hold the data of different scopes, so that the sub-components under the corresponding scopes can get the corresponding data and the corresponding updates.
-
Bottom up: Through the custom Notification class. Notification(data).dispatch(context) is published in the child component, and NotificationListener is captured and listened on the corresponding context.
The last
Through three articles, the necessary introduction to the Flutter documentation is supplemented with some details. There are no gestures, network requests, Channel and Native communication, animations, etc. Please study in conjunction with documents.
After the theoretical knowledge has been enriched, we will begin with the practical analysis of Flutter in the next part.
Refer to the article
Build Reactive Mobile apps in Flutter — Companion article
set-up-inherited-widget-app-state
Insight into Flutter interface development (highly recommended) (PS: Really highly recommended)
From 0 to 1: my Flutter technology practice | the nuggets essay, the campaign is underway