20 for Component Intercommunication Data Transfer You Only Need to Remember These Three Tricks

20 For Component Intercommunication Data Transfer You Only Need to Remember These Three Tricks #

Hello, I’m Chen Hang.

In the previous article, I showed you how to handle user interaction events (gestures) in Flutter. Gesture handling in Flutter can be divided into two types: handling raw pointer events and advanced gesture recognition.

Raw pointer events are distributed using a bubbling mechanism and can be listened to using a Listener widget. On the other hand, gesture recognition is handled using the Gesture widget. However, it’s important to note that although Flutter can support multiple gestures at the same time (including a widget listening to multiple gestures or multiple widgets listening to the same gesture), ultimately only one widget’s gesture can respond to user actions. To change this behavior, we need to customize gestures and modify the default behavior of the gesture arena for multi-gesture priority judgment.

In addition to responding to external events, another important task for the UI framework is to manage data synchronization between different components. Especially for Flutter, a framework that heavily relies on the behavior of composing widgets to create user interfaces, it becomes even more important to ensure that data changes are reflected in the final visual effects. Therefore, in today’s article, I will introduce how to pass data between components in Flutter.

In previous discussions, we have already achieved various interface layouts by combining and nesting widgets and customizing the visual properties of basic widgets using data. So you should have already realized that the standard way to pass data between components in Flutter is through property passing.

However, for more complex UI styles, especially when the view hierarchy is deep, a property may need to pass through many layers in order to reach a child component. This method of passing data would require many unnecessary components in the middle to also receive the data from their child widgets, making it cumbersome and redundant.

Therefore, for cross-layer data passing, Flutter also provides three solutions: InheritedWidget, Notification, and EventBus. Next, I will explain these three solutions to you one by one.

InheritedWidget #

InheritedWidget is a functional widget in Flutter that is used for sharing data in the widget tree. With InheritedWidget, we can efficiently pass data across different layers of the widget tree.

In my previous article, “Night Mode - How to Customize the App Theme in Different Styles?,” I introduced how to access the current theme style through the Theme widget to achieve style reuse, such as Theme.of(context).primaryColor.

The Theme class is a typical example of implementing InheritedWidget. In child widgets, we use the Theme.of method to find the parent Theme widget and obtain its properties. At the same time, we establish an observer relationship between the child widget and the parent widget. When the properties of the parent widget are modified, the child widget will also trigger an update.

Next, I will use the counter in the Flutter project template as an example to explain how to use InheritedWidget.

  • First, to use InheritedWidget, we define a new class CountContainer that inherits from it.

  • Then, we put the count property of the counter state into CountContainer and provide an of method to allow its child widgets to find it in the widget tree.

  • Finally, we override the updateShouldNotify method. This method is called by Flutter to determine whether an InheritedWidget needs to be rebuilt and update the data for the lower-level observer components. Here, we simply compare whether the count is equal.

    class CountContainer extends InheritedWidget { // Allow child widgets to find it in the widget tree static CountContainer of(BuildContext context) => context.inheritFromWidgetOfExactType(CountContainer) as CountContainer;

    final int count;

    CountContainer({ Key key, @required this.count, @required Widget child, }): super(key: key, child: child);

    // Determine whether an update is needed @override bool updateShouldNotify(CountContainer oldWidget) => count != oldWidget.count; }

Then, we use CountContainer as the root node and initialize count with 0. Later in its child widget Counter, we use the InheritedCountContainer.of method to find it and retrieve the count state to display it:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
   // Use CountContainer as the root node and initialize count with 0
    return CountContainer(
      count: 0,
      child: Counter()
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get the InheritedWidget node
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("InheritedWidget demo")),
      body: Text(
        'You have pushed the button this many times: ${state.count}',
      ),
    );
}

When you run the app, the result is shown in the following image:

Image 1: Example of using InheritedWidget

As you can see, using InheritedWidget is quite simple. No matter where Counter is in the widget tree under CountContainer, it can access the count property of its parent widget without manually passing the property.

However, InheritedWidget only provides the ability to read data. If we want to modify the data, we need to use it together with the State of StatefulWidget. We need to move all the data in InheritedWidget and the related data modification methods to the State object of StatefulWidget, while leaving only the references in InheritedWidget.

In the modified code snippet above, we removed the count property held by CountContainer and added a reference to the data holder State and the data modification method:

class CountContainer extends InheritedWidget {
  ...
  final _MyHomePageState model; // Use the State of MyHomePage directly to retrieve data
  final Function() increment;

  CountContainer({
    Key key,
    @required this.model,
    @required this.increment,
    @required Widget child,
  }): super(key: key, child: child);
  ...
}

Then, we put the count data and its corresponding modification method in the State, and still use CountContainer as the root node to initialize the data and the modification method.

In the child widget Counter, we still use the InheritedCountContainer.of method to find it, synchronize the count state with the UI, and synchronize the button’s click event with the data modification:

class _MyHomePageState extends State<MyHomePage> {
  int count = 0;
  void _incrementCounter() => setState(() {count++;}); // Modify the counter

  @override
  Widget build(BuildContext context) {
    return CountContainer(
      model: this, // Pass itself as the model to CountContainer
      increment: _incrementCounter, // Provide the method to modify the data
      child: Counter()
    );
  }
}

class Counter extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // Get the InheritedWidget node
    CountContainer state = CountContainer.of(context);
    return Scaffold(
      ...
      body: Text(
        'You have pushed the button this many times: ${state.model.count}', // Related data read method
      ),
      floatingActionButton: FloatingActionButton(onPressed: state.increment), // Related data modification method
    );
  }
}

When you run the app, you can see that we have achieved reading and writing the InheritedWidget data.

Image 2: Example of modifying InheritedWidget data

Notification #

Notification is another important mechanism in Flutter for cross-layer data sharing. If the data flow of InheritedWidget is from parent widget to child widget layer by layer, then Notification is the opposite, the data flow is from child widget to parent widget. This data transfer mechanism is suitable for scenarios where the child widget’s state changes and sends notifications.

In the previous article “Classic Widgets Part 2: What is UITableView/ListView in Flutter?”, I introduced the usage of ScrollNotification: when ListView is scrolling, it dispatches notifications, and we can use NotificationListener at the higher level to listen to ScrollNotification and handle it based on its state.

Listening to custom notifications is no different from ScrollNotification. However, to implement custom notifications, we first need to inherit from the Notification class. The Notification class provides a dispatch method, which allows us to send notifications up the element tree corresponding to the context layer by layer.

Next, let’s take a look at a specific example. In the code below, we have defined a custom notification and a child widget. The child widget is a button that sends a notification when clicked:

class CustomNotification extends Notification {
  CustomNotification(this.msg);
  final String msg;
}

// Extract a child widget to send notifications
class CustomChild extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // Dispatch notification when the button is clicked
      onPressed: () => CustomNotification("Hi").dispatch(context),
      child: Text("Fire Notification"),
    );
  }
}

In the parent widget of the child widget, we listen to this notification. Once we receive the notification, we trigger a screen refresh to display the received notification:

class _MyHomePageState extends State<MyHomePage> {
  String _msg = "Notification: ";
  @override
  Widget build(BuildContext context) {
    // Listen to the notification
    return NotificationListener<CustomNotification>(
        onNotification: (notification) {
          setState(() {_msg += notification.msg + "  ";}); // Received notification from the child widget, update msg
        },
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[Text(_msg), CustomChild()], // Add the child widget to the view tree
        )
    );
  }
}

Run the code, and you will see that every time you click the button, the latest notification information will appear on the screen:

Custom Notification

Figure 3. Custom Notification

EventBus #

Both InheritedWidget and Notification require the use of a Widget tree, which means that data sharing can only happen between Widgets that have a parent-child relationship. However, there is a common scenario where components do not have a parent-child relationship. This is where the event bus, EventBus, comes into play.

The event bus is a mechanism in Flutter that facilitates cross-component communication. It follows the publish/subscribe pattern, allowing subscribers to subscribe to events. When a publisher triggers an event, subscribers and the publisher can interact through the event. The publisher and subscribers do not need to have a parent-child relationship, and even non-Widget objects can publish or subscribe. These characteristics are similar to event bus mechanisms in other platforms.

Next, we will use an example of cross-page communication to demonstrate the specific usage of the event bus. It is important to note that EventBus is a third-party plugin, so we need to declare it in the pubspec.yaml file:

dependencies:
  event_bus: 1.1.0

EventBus is highly flexible and can support the transfer of any object. Therefore, in this example, we chose a custom event class, CustomEvent, with a string property as the payload:

class CustomEvent {
  String msg;
  CustomEvent(this.msg);
}

Then, we define a global eventBus object and listen to CustomEvent events in the first page. When the event is received, the UI will be updated. It is important to remember to clean up the event registration when the State is destroyed; otherwise, the State will be held by the EventBus indefinitely, causing a memory leak:

// Establish a global event bus
EventBus eventBus = new EventBus();

// First page
class _FirstScreenState extends State<FirstScreen> {
  String msg = "Notification: ";
  StreamSubscription subscription;

  @override
  initState() {
    // Listen for CustomEvent events and update UI
    subscription = eventBus.on<CustomEvent>().listen((event) {
      setState(() {
        msg += event.msg;
      });
    });
    super.initState();
  }

  @override
  dispose() {
    subscription.cancel(); // Clean up the registration when State is destroyed
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Text(msg),
      ...
    );
  }
}

Finally, in the second page, we trigger the CustomEvent event through a button click callback:

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      ...
      body: RaisedButton(
        child: Text('Fire Event'),
        // Trigger CustomEvent event
        onPressed: () => eventBus.fire(CustomEvent("hello")),
      ),
    );
  }
}

Run the application, click the button on the second page several times, and then return to view the message on the first page:

Image 4: EventBus Example

As you can see, the usage of EventBus is relatively simple, with fewer usage restrictions.

I have prepared a table summarizing the characteristics and use cases of the four data sharing methods: property passing, InheritedWidget, Notification, and EventBus. You can refer to it:

Image 5: Comparison of Property Passing, InheritedWidget, Notification, and EventBus for Data Sharing

Summary #

Alright, that’s it for today’s sharing. Let’s briefly review how to achieve cross-component data sharing in Flutter.

Firstly, we got to know InheritedWidget. For UI styles with a deep hierarchy, passing values through properties directly would result in the addition of redundant properties in many intermediate layers. InheritedWidget allows child widgets to share properties from their parent widget. It is worth noting that the properties in InheritedWidget can only be read in child widgets. If there is a scenario that requires modification, we need to use it together with the State of StatefulWidget.

Next, we learned about Notification, a mechanism for sharing data between layers from bottom to top. We can use NotificationListener to allow the parent widget to listen to events from the child widget.

Lastly, I introduced you to EventBus, a data synchronization mechanism that does not require a parent-child relationship between publishers and subscribers.

I have put the demos of these three cross-component data sharing methods mentioned in today’s sharing on GitHub. You can download them and run them yourself to appreciate their similarities and differences.

Here is the link to the demos on GitHub.

Thinking question #

Finally, I will leave you with a thinking question.

Please summarize the advantages and disadvantages of property passing, InheritedWidget, Notification, and EventBus separately.

Feel free to leave a comment in the comment section to share your opinions, and I will be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends to read together.