Several ways of transmitting values on the fluent page

Time:2021-10-13

Today, let’s talk about several ways of transmitting values on the fluent page:

  • InheritWidget
  • Notification
  • Eventbus

(current fluent version: 2.0.4)

InheritWidget

If you have seen the source code of the provider, you know that the principle of cross component value transfer of the provider is implemented according to the inheritwidget provided by the system. Let’s take a look at this component.
Inheritwidget is an abstract class. We write a class that stores user information. Userinfoinheritwidget inherits from inheritwidget:


class UserInfoInheritWidget extends InheritedWidget {

  UserInfoBean userInfoBean;
  UserInfoInheritWidget({Key key, this.userInfoBean, Widget child}) : super (child: child);

  static UserInfoWidget of(BuildContext context){
    return context.dependOnInheritedWidgetOfExactType<UserInfoWidget>();
  }
  
  @override
  bool updateShouldNotify(UserInfoInheritWidget oldWidget) {
    return oldWidget.userInfoBean != userInfoBean;
  }
}

We define a static method here: of, and pass in a context. We get the current class according to the context and get the userinfobean in the current class. In fact, we get the theme data according to the inheritwidget. In this way, we get the theme. Of (context). As for the method of, updateshouldnotify is a refresh mechanism. When to refresh the data

There is also an entity for user information:


class UserInfoBean {
  String name;
  String address;
  UserInfoBean({this.name, this.address});
}

We make two pages. The first page displays user information and a button. Click the button to jump to the second page, which also displays user information:

class Page19PassByValue extends StatefulWidget {
  @override
  _Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PassByValue'),
      ),
      body: DefaultTextStyle(
        style: TextStyle(fontSize: 30, color: Colors.black),
        child: Column(
          children: [
            Text(UserInfoWidget.of(context)!.userInfoBean.name),
            Text(UserInfoWidget.of(context)!.userInfoBean.address),
            SizedBox(height: 40),
            TextButton(
              Child: text ('click to jump '),
              onPressed: (){
                Navigator.of(context).push(CupertinoPageRoute(builder: (context){
                  return DetailPage();
                }));
              },
            )
          ],
        ),
      ),
    );
  }
}
class DetailPage extends StatefulWidget {
  @override
  _DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Detail'),
      ),
      body: DefaultTextStyle(
        style: TextStyle(fontSize: 30, color: Colors.black),
        child: Center(
          child: Column(
            children: [
              Text(UserInfoWidget.of(context).userInfoBean.name),
              Text(UserInfoWidget.of(context).userInfoBean.address),
              TextButton(
                    onPressed: () {
                      setState(() {
                        UserInfoWidget.of(context)!.updateBean('wf123','address123');
                      });
                    },
                    Child: text ('click to modify '))
            ],
          ),
        ),
      )
    );
  }
}

Since we transfer values across components here, we need to put the userinfowidget on the upper layer of the materialapp and give the userinfobean an initial value:


class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return UserInfoWidget(
      userInfoBean: UserInfoBean(name: 'wf', address: 'address'),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
          visualDensity: VisualDensity.adaptivePlatformDensity,
        ),
        home: MyHomePage(title: 'Flutter Demo Home Page'),
      ),
    );
  }
}

In this way, a cross component value transfer is realized, but there is another problem. When we assign the value to the userinfowidget, it is at the top level. In the real business scenario, if we assign the value of userinfo to the materialapp, we have not got the user data yet, so we need a method to update the userinfo and refresh it immediately after modification, With the help of setstate, we can change the name of the userinfowidget defined above and package it in statefulwidget:


class _UserInfoInheritWidget extends InheritedWidget {

  UserInfoBean userInfoBean;
  Function update;
  _UserInfoInheritWidget({Key key, this.userInfoBean, this.update, Widget child}) : super (child: child);

  updateBean(String name, String address){
    update(name, address);
  }

  @override
  bool updateShouldNotify(_UserInfoInheritWidget oldWidget) {
    return oldWidget.userInfoBean != userInfoBean;
  }
}

class UserInfoWidget extends StatefulWidget {
  UserInfoBean userInfoBean;
  Widget child;
  UserInfoWidget({Key key, this.userInfoBean, this.child}) : super (key: key);

  static _UserInfoInheritWidget of(BuildContext context){
    return context.dependOnInheritedWidgetOfExactType<_UserInfoInheritWidget>();
  }
  @override
  State<StatefulWidget> createState() => _UserInfoState();
}

class _UserInfoState extends State <UserInfoWidget> {

  _update(String name, String address){
    UserInfoBean bean = UserInfoBean(name: name, address: address);
    widget.userInfoBean = bean;
    setState(() {});
  }
  @override
  Widget build(BuildContext context) {
    return _UserInfoInheritWidget(
      child: widget.child,
      userInfoBean: widget.userInfoBean,
      update: _update,
    );
  }
}

Above, the class inherited from inheritwidget is changed to a name:_ The userinfoinheritwidget only exposes the userinfowidget encapsulated with statefulwidget to the public_ Userinfoinheritwidget passes in the update data method containing setstate. When updating data, get the data inherited from the inheritwidget through userinfowidget. Of (context)_ Userinfoinheritwidget class, calling updatebean method actually calls the method containing setstate, so it achieves data update and page refresh

Let’s focus on how userinfowidget. Of (context) obtains objects inherited from inheritwidget class. By looking at similar methods: theme. Of (context) finds that it is based on dependoninheritedwidgetofexacttype, so we also obtain it as it is_ Userinfoinheritwidget, click the dependoninheritedwidgetofexacttype source code and find that it jumps to buildcontext and defines this method:


  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });

Students who know the relationship among widget, element and renderobject know that in fact, context is an example of element, which is also mentioned in the comments of buildcontext:

We can find the implementation of this method in element:


@override
  T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    final InheritedElement? ancestor = _inheritedWidgets == null ? null : _inheritedWidgets![T];
    if (ancestor != null) {
      assert(ancestor is InheritedElement);
      return dependOnInheritedElement(ancestor, aspect: aspect) as T;
    }
    _hadUnsatisfiedDependencies = true;
    return null;
  }

_ Where do inherited widgets come from? Let’s search and find them in element


void _updateInheritance() {
    assert(_lifecycleState == _ElementLifecycle.active);
    _inheritedWidgets = _parent?._inheritedWidgets;
  }

Take another look_ When was the updateinheritance method called

@mustCallSuper
  void mount(Element? parent, dynamic newSlot) {
    ...
    ... omit irrelevant code
    _parent = parent;
    _slot = newSlot;
    _lifecycleState = _ElementLifecycle.active;
    _depth = _parent != null ? _parent!.depth + 1 : 1;
    if (parent != null) // Only assign ownership if the parent is non-null
      _owner = parent.owner;
    final Key? key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _ updateInheritance();// This is called once
  }

also:

@mustCallSuper
  void activate() {
    ...
    ... irrelevant code omitted
    final bool hadDependencies = (_dependencies != null && _dependencies!.isNotEmpty) || _hadUnsatisfiedDependencies;
    _lifecycleState = _ElementLifecycle.active;
    _dependencies?.clear();
    _hadUnsatisfiedDependencies = false;
    _ updateInheritance();// Here's another call
    if (_dirty)
      owner!.scheduleBuildFor(this);
    if (hadDependencies)
      didChangeDependencies();
  }

From the above code, we can see that the element of each page will pass_ The parent passes the parent information to the child, and our userinfowidget is saved in the_ In parent_ In the inheritedwidgets collection: Map < type, inheritedelement >_ inheritedWidgets;, When_ When inheritedwidgets are passed down in the page tree, if the current widget is an inheritwidget, look first in the element corresponding to the current widget_ From the parent_ Whether inheritedwidwidgets is empty. If it is empty, create a new collection, save yourself to this collection, and use the current type as the key (which is why the context.dependoninheritedwidgetofexacttype method in the of method is called to transfer the current type)_ Remove values from the inheritedwidgets collection; If it is not empty, you can save yourself directly, which is the principle of of.

Notification

The inheritwidget mentioned above generally transfers values from the root component to the child component, and notification transfers values from the child component to the parent component. Let’s take a look at its usage

class Page19PassByValue extends StatefulWidget {
  @override
  _Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
  UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PassByValue'),
      ),
      body: Center(
        child: NotificationListener<MyNotification>(
          onNotification: (MyNotification data) {
            userInfoBean = data.userInfoBean;
            setState(() {});
            ///A bool value needs to be returned here. True means to prevent the event from continuing to pass up, and false means that the event can continue to pass up to the parent component
            return true;
          },
          child: Builder(
          ///Here, a builder is used to wrap it in order to get it
          ///Context of notificationlistener
            builder: (context) {
              return Column(
                children: [
                  Text(userInfoBean.name),
                  Text(userInfoBean.address),
                  Container(
                    child: FlatButton(
                      Child: text ('click to transfer value '),
                      onPressed: () {
                        MyNotification(userInfoBean: UserInfoBean(name: 'wf123', address: 'address123')).dispatch(context);
                      },
                    ),
                  )
                ],
              );
            },
          ),
        ),
      ),
    );
  }
}
///Notification is an abstract class,
///To use notification, you need to customize a class to inherit notification
class MyNotification extends Notification {
  UserInfoBean userInfoBean;
  MyNotification({this.userInfoBean}) : super();
}

Let’s take a look at the dispatch method in the source code:


void dispatch(BuildContext target) {
    // The `target` may be null if the subtree the notification is supposed to be
    // dispatched in is in the process of being disposed.
    target?.visitAncestorElements(visitAncestor);
  }

Target is the context we passed in, that is, we called the visitancessorelements method of buildcontext, passed the visitancessor method as a parameter, and the visitancessor method returned a bool value:


  @protected
  @mustCallSuper
  bool visitAncestor(Element element) {
    if (element is StatelessElement) {
      final StatelessWidget widget = element.widget;
      if (widget is NotificationListener<Notification>) {
        if (widget._dispatch(this, element)) // that function checks the type dynamically
          return false;
      }
    }
    return true;
  }

Let’s go inside element to see the implementation of visitancestorelements method:


@override
  void visitAncestorElements(bool visitor(Element element)) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element? ancestor = _parent;
    while (ancestor != null && visitor(ancestor))
      ancestor = ancestor._parent;
  }

When there is a parent node and the visitor method returns true, execute the while loop. The visitor is the method passed in by the notification class. Look back at the implementation of the visitor method. When the ancestor passed from the element to the visitor method is the notificationlister class, judge the widget_ Dispatch method, and widget_ Dispatch method:


final NotificationListenerCallback<T>? onNotification;

  bool _dispatch(Notification notification, Element element) {
    if (onNotification != null && notification is T) {
      final bool result = onNotification!(notification);
      return result == true; // so that null and false have the same effect
    }
    return false;
  }

This is the implementation of the onnotification method we wrote outside. The onnotification method we implemented outside returns true (that is, prevent the event from continuing to pass up). The above while loop is mainly used to execute the methods in our onnotification

To sum up, mynotification executes the dispatch method, passes the context, finds the corresponding notificationlister from the parent according to the current context, and executes the onnotification method in the notificationlister. If true is returned, the event will no longer be passed to the parent. If false is returned, the event will continue to pass to the previous notificationlister and execute the corresponding method. Notification is mainly used in the same page. The child passes values to the parent, which is relatively lightweight. However, if we use a provider, we may directly pass values through the provider.

Eventbus

Eventbus is used for two different pages. It can transfer values across multi-level pages. The usage is also relatively simple. I created an eventbusutil to create a singleton
import ‘package:event_bus/event_bus.dart’;


class EventBusUtil {
  static  EventBus ? _instance;
  static EventBus getInstance(){
    if (_instance == null) {
      _instance = EventBus();
    }
    return _instance!;
  }
}

Listen on the first page:

class Page19PassByValue extends StatefulWidget {
  @override
  _Page19PassByValueState createState() => _Page19PassByValueState();
}

class _Page19PassByValueState extends State<Page19PassByValue> {
  UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
  @override
  void initState() {
    super.initState();
    EventBusUtil.getInstance().on<UserInfoBean>().listen((event) {
      setState(() {
        userInfoBean = event;
      });
    });
  }
  
  @override
  void dispose() {
    super.dispose();
    //Remember to turn it off when not in use
    EventBusUtil.getInstance().destroy();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PassByValue'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(userInfoBean.name),
            Text(userInfoBean.address),
            TextButton(onPressed: (){
              Navigator.of(context).push(CupertinoPageRoute(builder: (_){
                return EventBusDetailPage();
              }));
            }, child: text ('click to jump '))

          ],
        ),
      ),
    );
  }
}

Send events on the second page:

class EventBusDetailPage extends StatefulWidget {
  @override
  _EventBusDetailPageState createState() => _EventBusDetailPageState();
}

class _EventBusDetailPageState extends State<EventBusDetailPage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('EventBusDetail'),
      ),
      body: Center(
        child: TextButton(onPressed: (){
          EventBusUtil.getInstance().fire(UserInfoBean(name: 'name EventBus', address: 'address EventBus'));
        }, child: text ('click to transfer value '),
      ),
    );
  }
}

Let’s take a look at the source code of eventbus and find that there are only dozens of lines of code. It creates a streamcontroller to realize cross component value transfer through streamcontroller. We can also directly use this streamcontroller to realize page value transfer:

class Page19PassByValue extends StatefulWidget {
  @override
  _Page19PassByValueState createState() => _Page19PassByValueState();
}

StreamController controller = StreamController();

class _Page19PassByValueState extends State<Page19PassByValue> {
  
  //Set an initial value
  UserInfoBean userInfoBean = UserInfoBean(name: 'wf', address: 'address');
  @override
  void initState() {
    super.initState();
    controller.stream.listen((event) {
      setState(() {
        userInfoBean = event;
      });
    });
  }

  @override
  void dispose() {
    super.dispose();
    //Remember to close the page when it is destroyed
    controller.close();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('PassByValue'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(userInfoBean.name),
            Text(userInfoBean.address),
            TextButton(onPressed: (){
              Navigator.of(context).push(CupertinoPageRoute(builder: (_){
                return MyStreamControllerDetail();
              }));
            }, child: text ('click to jump '))
          ],
        ),
      )
    );
  }
}
class MyStreamControllerDetail extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MyStreamControllerDetailState();
  }
}
class _MyStreamControllerDetailState extends State <MyStreamControllerDetail> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('StreamController'),
      ),
      body: Center(
        child: TextButton(onPressed: (){
        //When you return to the previous page, you will find that the data on the page has changed
          controller.sink.add(UserInfoBean(name: 'StreamController pass name: 123', address: 'StreamController pass address 123'));
        }, child: text ('click to transfer value '),
      ),
    );
  }
}

This is the end of this article on several ways of value transfer on the shutter page. For more information about value transfer on the shutter page, please search the previous articles of developeppaer or continue to browse the relevant articles below. I hope you will support developeppaer in the future!