Fluent learning functional Widget

Posted by stridox on Thu, 30 Dec 2021 12:55:03 +0100

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:

  1. 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
  2. The messaging of data changes is blocked
  3. 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

Topics: Flutter