1. WillPopScope
It is a component of navigation return interception, similar to the onBackPress method encapsulated in Android. Take a look at its constructor:
class WillPopScope extends StatefulWidget { const WillPopScope({ Key? key, required this.child, required this.onWillPop, })
onWillPop is a callback function. When the user clicks this button, it will be called back. This function needs to return a Future object. If the final value of Future is false, the current route will not be out of the stack. If the final value is true, the current route will be out of the stack
1.1 example
The following code is an example of closing the return key interception of the current page to prevent accidental touch. If the return button is clicked twice within 1s, it will exit. If it exceeds 1s, it will be timed again:
class _WillPopScopeRouteState extends State<WillPopScopeRoute> { // Last click time DateTime? _lastPressedAt; @override Widget build(BuildContext context) { return Scaffold( body: WillPopScope( onWillPop: () async { if (_lastPressedAt == null || DateTime.now().difference(_lastPressedAt!) > const Duration(seconds: 1)) { _lastPressedAt = DateTime.now(); return false; } return true; }, child: Container( alignment: Alignment.center, child: Text("1s Press the return key twice in a row to exit"), ), )); } }
2. InheritedWidget
The component used for data sharing provides a way to share data from top to bottom in the Widget tree. For example, if we share a data in the application root Widget through the InheritedWidget, we can obtain the shared data in any child Widget tree.
This feature is very convenient in some scenarios where data needs to be shared throughout the Widget. For example, Flutter uses this component to share application topics and Locale information.
2.1 didChangeDependencies
When learning StatefulWidget, we mentioned that the State object has a callback of didChangeDependencies, which will be called by the fluent framework when the "dependency" changes, and this "dependency" is whether the child Widget uses the data of inheritedwidwidget in the parent Widget. If it is used, it means that the child Widget has a dependency, If not, there is no such dependency.
This mechanism enables the InheritedWidget that the child component depends on to update itself when it changes. For example, when the theme and locale change, the didChangeDEpendencies method that depends on its child Widget will be called
Let's take a look at the InheritedWidget version of the calculator application in the official example:
class ShareDataWidget extends InheritedWidget { ShareDataWidget({Key? key, required this.data, required Widget child}) : super(key: key, child: child); // Shared data, representing the number of clicks final int data; // Provides a convenient method for obtaining shared data for child widgets in the tree static ShareDataWidget? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>(); } // This callback determines whether to notify the data dependent widget s in the subtree when the data changes @override bool updateShouldNotify(ShareDataWidget oldWidget) { return oldWidget.data != data; } } class _TestWidget extends StatefulWidget { @override State<_TestWidget> createState() => _TestWidgetState(); } class _TestWidgetState extends State<_TestWidget> { @override Widget build(BuildContext context) { return Text(ShareDataWidget.of(context)!.data.toString()); } @override void didChangeDependencies() { super.didChangeDependencies(); // Called when the InheritedWidget in the parent or ancestor Widget changes // If there is no dependency in build, the callback will not be called print("Dependency has changed"); } } class InheritedWidgetTestRoute extends StatefulWidget { @override _InheritedWidgetTestRouteState createState() => _InheritedWidgetTestRouteState(); } class _InheritedWidgetTestRouteState extends State<InheritedWidgetTestRoute> { int cnt = 0; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ShareDataWidget( data: cnt, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.only(bottom: 25), child: _TestWidget()), ElevatedButton( onPressed: () => setState(() { ++cnt; }), child: const Text("Self increasing")) ], ), ), ), ); } }
After operation, whenever you click the auto increment button, the printing table will print:
If_ If the data in ShareDataWidget is not used in TestWidget, its didChangeDependencies() will not be called because it does not depend on its data.
What can be done in didChangeDependencies?
Generally speaking, child widgets rarely rewrite this method, because after the dependency changes, the fluent framework will also call the build method to rebuild the component tree. However, if you need to perform some expensive operations after the dependency changes, such as database storage or network library requests, the best way is to execute this method, This can avoid performing these expensive operations every build.
2.2 learn more about InheritedWidget
If we just want to_ Called when the TestWidgetState refers to the data of the ShareDataWidget but does not want the ShareDataWidget to change_ What should the TestWidgetState method do?
We just need to change sharedatawidget Implementation of ():
static ShareDataWidget? of(BuildContext context) { // return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>(); return context.getElementForInheritedWidgetOfExactType<ShareDataWidget>()!.widget as ShareDataWidget; }
The only change is to replace the dependOnInheritedWidgetOfExactType method with getElementForInheritedWidgetOfExactType. What's the difference between them? Let's look at the source code of these two methods:
@override InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>() { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T]; return ancestor; } @override T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T]; if (ancestor != null) { // The dependOnInheritedElement method is called more than the former return dependOnInheritedElement(ancestor, aspect: aspect) as T; } _hadUnsatisfiedDependencies = true; return null; }
Take a look at the dependOnInheritedElement:
@override InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }) { assert(ancestor != null); _dependencies ??= HashSet<InheritedElement>(); _dependencies!.add(ancestor); ancestor.updateDependencies(this, aspect); return ancestor.widget; }
The dependOnInheritedElement method mainly registers dependencies and adds them to a HashSet. getElementForInheritedWidgetOfExactType() does not.
It should be noted that in the above example, if the implementation mode of getElementForInheritedWidgetOfExactType is changed, you will find that_ The didChangeDependencies of TestWidgetState will not be called again, but the build method will be called. This is because after clicking the auto increment button, setState will be called to reconstruct the whole page, and_ TestWidget is not cached, so it will also be rebuilt, so the build method will be called
Then a new problem is introduced: in fact, we only want to update the child nodes that depend on ShareDataWidget in the child tree, and calling the setState method of the parent component (here _InheritedWidgetTestRouteState) will inevitably lead to the build of all child nodes. This will cause unnecessary waste and problems.
Caching data can solve this problem by encapsulating a StatefulWidget to cache the child Widget tree. The following is to implement a Provider to demonstrate.
3. Provider
The idea of the Provider package is to save the state that needs to be shared across components in the InheritedWidget, and then the child components reference the InheritedWidget. The InheritedWidget will bind the child components to generate dependencies, and then automatically update the child components when the data changes.
In order to enhance understanding, we do not directly look at the implementation of the Provider, but practice which Provider has the smallest function
3.1 implementation of simple Provider
Generics are introduced here so that the outside world can save more general data
class InheritedProvider<T> extends InheritedWidget { InheritedProvider({required this.data, required Widget child}) : super(child: child); final T data; @override bool updateShouldNotify(covariant InheritedWidget oldWidget) { // Return true here first return true; }
The second step is to realize "how to change the data when it changes?", The method here is to add listeners by using. There is ChangeNotifier in fluent, which inherits from Listenable. It is a publish subscriber mode. Add listeners by addListener and removeListener, and trigger the callback of listeners by notifyListener.
Therefore, we put the shared state into a Model class, and then let it inherit from ChangeNotifier. In this way, when the shared state changes, we only need to call notify to notify the subscriber, and the subscriber can rebuild the InheritedProvider:
class ChangeNotifierProvider<T extends ChangeNotifier> extends StatefulWidget { ChangeNotifierProvider({Key? key, required this.data, required this.child}); final Widget child; final T data; static T of<T>(BuildContext context) { final provider = context.dependOnInheritedWidgetOfExactType<InheritedProvider<T>>(); return provider!.data; } @override State<StatefulWidget> createState() => _ChangeNotifierProviderState<T>(); }
This class inherits from StatefulWidget and provides the of method for subclasses to easily obtain the shared State saved in InheritedProvider in the Widget tree. The following is to implement the State class corresponding to this class:
class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider> { void update() { setState(() { // If the data changes, rebuild the InheritedProvider }); } @override void didUpdateWidget( covariant ChangeNotifierProvider<ChangeNotifier> oldWidget) { if (widget.data != oldWidget.data) { // When the Provider is updated, if the old and new data are different, unbind and intercept the data monitor, and add a new data monitor at the same time oldWidget.data.removeListener(update); widget.data.addListener(update); } super.didUpdateWidget(oldWidget); } @override void initState() { // Add listener to model widget.data.addListener(update); super.initState(); } @override void dispose() { widget.data.removeListener(update); super.dispose(); } @override Widget build(BuildContext context) { return InheritedProvider( data: widget.data, child: widget.child, ); } }
As you can see_ The main function of ChangeNotifierProviderState class is to rebuild the Widget tree when listening to the change of sharing state. In_ Calling setState method in ChangeNotiferProviderState, Widget. The child is always the same, so when building, the child of InheritedProvider always refers to the same child Widget, so the Widget The child will not be rebuilt, which is equivalent to caching the child. Of course, if the parent Widget of ChangeNotifierProvider is rebuilt, the passed in child may change.
Next, we use this component to implement a shopping cart example.
3.1. 1 shopping cart example
We need to implement a function to display the total price of all goods in the shopping cart, and this price is obviously the state we want to share, because the price of the shopping cart will change with the addition and removal of goods.
Let's define an Item class to represent product information:
class Item { Item(this.price, this.count); // item pricing double price; // Quantity of goods int count; }
Next, define a CartModel class that stores the product data in the shopping cart:
class CartModel extends ChangeNotifier { final List<Item> _items = []; // It is forbidden to change the information in the shopping cart UnmodifiableListView<Item> get items => UnmodifiableListView(_items); // Total price double get totalPrice => _items.fold( 0, (previousValue, element) => previousValue + element.count * element.price); // Add [item] to the shopping cart. The function of this method is to change the shopping cart externally void add(Item item) { _items.add(item); // Notify subscribers to rebuild InheritedProvider to update status notifyListeners(); } }
This CartModel is the data type we need to share across components. Finally, we write an example page:
class _ProviderRouteState extends State<ProviderRoute> { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: ChangeNotifierProvider<CartModel>( data: CartModel(), child: Builder(builder: (context) { return Column( children: [ Builder(builder: (context) { var cart = ChangeNotifierProvider.of<CartModel>(context); return Text("The total price is:${cart?.totalPrice}"); }), Builder(builder: (context) { print("ElevatedButton build"); return ElevatedButton( onPressed: () { ChangeNotifierProvider.of<CartModel>(context) ?.add(Item(15, 1)); }, child: Text("Add item")); }) ], ); }), ), ), ); } }
Next, every time you click the add item button, you will increase 15 yuan. Generally speaking, the routing advantage of ChangeNotifierProvider as the whole App will be very obvious. It can share data to the whole App. The Provider model is shown in the following figure:
The benefits of using Provider include:
- The business code value is concerned with data update. It only needs to update the Model, and the UI will be updated automatically. There is no need to manually call setState to display the refresh page after the state changes
- The messaging of data changes is blocked
- In large and complex scenarios, using global shared variables will simplify code logic
4. Theme
ThemeData is used to save the theme data in the Material component library. It contains customizable parts. We can customize the application theme through ThemeData. In sub components, we can use theme Of method to get the current ThemeData.
There are many definable properties of ThemeData. Here are some common construction properties:
ThemeData({ Brightness? brightness, //Dark or light MaterialColor? primarySwatch, //The theme color sample is described below Color? primaryColor, //The main color determines the color of the navigation bar Color? cardColor, //Card color Color? dividerColor, //Split line color ButtonThemeData buttonTheme, //Button theme Color dialogBackgroundColor,//Dialog box background color String fontFamily, //Text font TextTheme textTheme,// Font theme, including text styles such as title and body IconThemeData iconTheme, // Default style for Icon TargetPlatform platform, //Specify the platform and apply the platform specific control style ColorScheme? colorScheme, ... })
Let's implement a routing skin change function:
class _ThemeRouteState extends State<ThemeRoute> { // Current theme color MaterialColor _themeColor = Colors.teal; @override Widget build(BuildContext context) { ThemeData themeData = Theme.of(context); return Theme( data: ThemeData( // Color used for navigation bar, FloatingActionButton primarySwatch: _themeColor, // Color for Icon iconTheme: IconThemeData(color: _themeColor)), child: Scaffold( appBar: AppBar(title: Text("Subject test")), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // The first line Icon uses iconTheme in the topic Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.favorite), Icon(Icons.airport_shuttle), Text("Color follows theme") ], ), // The second line Icon custom color Theme( data: themeData.copyWith( iconTheme: themeData.iconTheme.copyWith(color: Colors.blue)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.favorite), Icon(Icons.airport_shuttle), Text("Color fixed blue") ], ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () => setState(() => _themeColor = _themeColor == Colors.teal ? Colors.green : Colors.teal), child: Icon(Icons.palette), ), ), ); } }
The effects are as follows:
We can override the global theme through the local theme. If we need to skin the whole application, we can modify the theme of MaterialApp
5. ValueListenableBuilder
InheritedWidget provides a top-down data sharing method, but some scenarios are not transferred from top to bottom, such as horizontal transfer or bottom to top. To solve this problem, fluent provides a ValueListenableBuilder component, which is used to listen to a data source. If the data source changes, its builder will be re executed.
Defined as:
const ValueListenableBuilder({ Key? key, required this.valueListenable, required this.builder, this.child, })
- valueListenable
Indicates a data source that can be listened to. The type is valuelistenable < T > - builder
When the data source changes, the builder will be called again to rebuild the subcomponent tree - child
The sub component tree will be rebuilt every time in the builder. If there are some unchanged parts in the sub component tree, it can be passed to child, and child will be passed to the builder as the third parameter of the builder. In this way, the component cache can be realized
5.1 example
class _ValueListenableState extends State<ValueListenableRoute> { final ValueNotifier<int> _counter = ValueNotifier<int>(0); static const double textScaleFactor = 1.5; @override Widget build(BuildContext context) { print("build"); return Scaffold( body: Center( child: ValueListenableBuilder<int>( builder: (context, value, child) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ child!, Text("$value times", textScaleFactor: textScaleFactor) ], ); }, child: const Text("click", textScaleFactor: textScaleFactor), valueListenable: _counter, ), ), floatingActionButton: FloatingActionButton( child: const Icon(Icons.add), onPressed: () => _counter.value++, ), ); } }
This is a demo of the counter. The build method is executed once when the page is opened. When the + sign is clicked, the real page is not rebuilt, but the component tree is rebuilt by VlaueListenableBuilder.
Therefore, the suggestion is to let ValueListenableBuilder only build widgets that depend on data sources as much as possible, which can narrow the scope of construction, that is, the splitting granularity of ValueListenableBuilder can be finer
6. Asynchronous UI update
Many times, we rely on some asynchronous data to dynamically update the UI. For example, we need to obtain Http data first, and then display a load box in the process of obtaining the data. When we get the data, we can render the page, or we want to show the progress of Stream. Fluent provides FutureBuilder and StreamBuilder components to quickly implement these two functions. The examples are relatively simple and will not be listed here