2
头图

Observer mode , also known as publish and subscribe mode, is a behavioral design mode-you can define a subscription mechanism that can notify multiple when an object event occurs to observe other objects of the object.

The observer pattern defines a one-to-many dependency relationship, allowing multiple observer objects to monitor a certain subject object at the same time.

When this subject object changes in state, it will notify all observer objects so that they can update themselves automatically.

From the definition, it is not difficult to find that observer and observer/publisher are the most important elements in this model.

The WeChat official account can be regarded as an example of the most typical observer pattern in life. If you subscribe to the "Flutter Community", whenever the Flutter community publishes an article, this message will be pushed to you and other subscribers. Among them, you are the observer , and the public account "Flutter Community" is the observer ( Observable) or Publisher (Subject) .

The observer pattern is often used in such event processing systems. From a conceptual understanding, the observer is often referred to as the event stream (stream of events) or event stream source (stream source of events) ) , and the observer is equivalent to event sink (sinks of events) .

At the same time, also to achieve observer pattern reactive programming basis, RxDart, EventBus other libraries are a product of the observer pattern.

Object-oriented

In object-oriented, observer and publisher (observed) correspond to objects of two classes (Observer and Subject) respectively.

观察者模式 UML 图,图源维基百科

subscription mechanism provided for each object to subscribe or unsubscribe from the publisher's event stream in the publish class (Subject), including:

  1. A list member variable used to store the subscriber object reference;
  2. Several public methods for adding or removing subscribers from this list.
// 被观察者
class Subject {
  List<Observer> _observers;
  Subject([List<Observer> observers]) {
    _observers = observers ?? [];
  }

  // 注册观察者
  void registerObserver(Observer observer) {
    _observers.add(observer);
  }
  
  // 解注册观察者
  void unregisterObserver(Observer observer) {
    _observers.remove(observer)
  }

  // 通知观察者
  void notifyobservers(Notification notification) {
    for (var observer in _observers) {
      observer.notify(notification);
    }
  }
}

At this point, whenever an event occurs, it only needs to traverse the subscriber and call the specific notification method of its object (such as the notifyobservers method in the above code).

In practical applications, a publisher usually corresponds to multiple subscribers, and publishers and subscribers should follow the object-oriented development and design principles, therefore:

  1. In order to avoid coupling, subscribers must implement the same interface;
  2. The publisher only interacts with the subscriber through this interface, and the interface method can declare parameters so that the publisher can pass some context data (such as the notification object in the code below) when sending a notification.
// 观察者
class Observer {
  String name;
  
  Observer(this.name);

  void notify(Notification notification) {
    print("[${notification.timestamp.toIso8601String()}] Hey $name, ${notification.message}!");
  }
}

In this way, we can get the following observer pattern implemented in Dart language. Here is a simple application:

// 具体的被观察者 CoffeeMaker
// 每当 Coffee 制作完成发出通知给观察者。
class CoffeeMaker extends Subject {
  CoffeeMaker([List<Observer> observers]) : super(observers);
  
  void brew() {
    print("Brewing the coffee...");
    notifyobservers(Notification.forNow("coffee's done"));
  }
}

void main() {
  var me = Observer("Tyler");
  var mrCoffee = CoffeeMaker(List.from([me]));
  var myWife = Observer("Kate");
  mrCoffee.registerObserver(myWife);
  mrCoffee.brew();
}

CoffeeMaker here inherits from Subject. As a specific publishing class, the brew() method is its internal method, which is used to notify other observers whenever coffee is made. The above code, we mrCoffee registered on this coffee machine myWife this observer, mrCoffee.brew(); after the trigger, myWife internal notify method will be called.

The observer pattern implements the publish and subscribe relationship between them. In practical applications, the event being processed by the observer is likely to be asynchronous, and as an observer, it does not have to be displayed to block waiting for the completion of the event. Instead, it is notified by the observer, and when the event is completed, the event is actively "pushed" to the observer who cares about the event. In contrast, there is a category of observers who will use background threads to poll and monitor the subject events they care about. We will not expand on this topic for now.

If the observer mode is used carelessly, the legendary failure listener problem may easily occur, which leads to a memory leak, because in the basic implementation, the observer still holds a strong reference to the observer. If the event is halfway, it is observed When the observer no longer exists or does not care about this event, the observer cannot be recycled. Therefore, in this case, we should make a mechanism for unsubscribing while being observed to release useless resources in time.

Dart

Stream can be regarded as one of the typical models of the observer pattern natively supported by the Dart language. It itself is Dart:async package. The responsive programming library RxDart is also encapsulated based on Stream.

Conceptually, we can think of Stream as a conveyor belt that can connect both ends. As a developer, we can put data on one end of the conveyor belt, and Stream will transmit the data to the other end.

Similar to the situation in reality, if no one at the other end of the conveyor belt accepts data, the data will be discarded by the program. Therefore, we usually arrange an object to receive data at the end of the transmission. In reactive programming, it is Known as the observer of the data.

If we say that in the object-oriented Dart above, the relationship between the observer and the observed is formed while keeping the coupling as low as possible, and is relatively independent. Then in reactive programming, their relationship is the closer relationship between upstream and downstream .

Because Stream, as the name suggests, means "stream". The observer generates events at the entrance of the stream, and the observer waits for the arrival of data or events at the exit of the stream.

In this process, subscription of the and the event publishing of the are directly completed in the Stream or within the framework.

In Dart, we can use StreamController to create a stream:

var controller = new StreamController<int>();

controller.add(1); // 将数据放入流中

As shown in the above code, StreamController , you must specify the generic type to define the data object that Stream controller can accept int type 06125c04f706d3. We can use its add method to put the data into its conveyor belt. .

If we run the above two lines of code directly, there will be no results in the end, because we have not yet set the object to receive data for the conveyor:

var controller = new StreamController<int>();

controller.stream.listen((item) => print(item)); // 数据观察者函数

controller.add(1);
controller.add(2);
controller.add(3);

In the above code, we can add observers who listen to the Stream event for the controller object by calling the listen method of the stream object inside the StreamController. This method accepts a callback function, and this callback function accepts the one we have in the new StreamController<int>() generic The declared data object is used as a parameter.

At this time, whenever we add method again, we will notify the observer, call this function, and print the passed data:

1
2
3

In addition, we can also make the observer stop monitoring the data passed in the Stream after a certain period of time. The listen function in the above code will return a StreamSubscription . When we call its .cancel() , the observation will be released. , No longer receive data:

var controller = new StreamController<String>();

StreamSubscription subscription = controller.stream.listen((item) => print(item));

controller.add(1);
controller.add(2);
controller.add(3);

await Future.delayed(Duration(milliseconds: 500));

subscription.cancel();

Flutter

ChangeNotifier

ChangeNotifier is probably the most typical example of the observer mode in Flutter. It is implemented from Listenable, maintains a _listeners list to store observers, and implements addListener and removeListener to complete its internal subscription mechanism:

class ChangeNotifier implements Listenable {
  LinkedList<_ListenerEntry>? _listeners = LinkedList<_ListenerEntry>();

  @protected
  bool get hasListeners {
    return _listeners!.isNotEmpty;
  }
  
  @override
  void addListener(VoidCallback listener) {
    _listeners!.add(_ListenerEntry(listener));
  }

  @override
  void removeListener(VoidCallback listener) {
    for (final _ListenerEntry entry in _listeners!) {
      if (entry.listener == listener) {
        entry.unlink();
        return;
      }
    }
  }

  @mustCallSuper
  void dispose() {
    _listeners = null;
  }

  @protected
  @visibleForTesting
  void notifyListeners() {
    if (_listeners!.isEmpty)
      return;
    final List<_ListenerEntry> localListeners = List<_ListenerEntry>.from(_listeners!);
    for (final _ListenerEntry entry in localListeners) {
      try {
        if (entry.list != null)
          entry.listener();
      } catch (exception, stack) {
        // ...
      }
    }
  }
}

In actual use, we only need to inherit ChangeNotifier to have this subscription mechanism, as shown in the following CartModel class:

class CartModel extends ChangeNotifier {
  final List<Item> _items = [];

  UnmodifiableListView<Item> get items => UnmodifiableListView(_items);

  int get totalPrice => _items.length * 42;

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }

  void removeAll() {
    _items.clear();
    notifyListeners();
  }
}

CartModel internal maintains a _items array, add , removeAll supplied to the external interface of the array method of operation, each time _items change is invoked notifyListeners() inform all its viewers.

ChangeNotifier as flutter:foundation most basic class, not dependent on any other upper class, it is also very simple test, we can target CartModel do a simple unit test:

test('adding item increases total cost', () {
  final cart = CartModel();
  final startingPrice = cart.totalPrice;
  cart.addListener(() {
    expect(cart.totalPrice, greaterThan(startingPrice));
  });
  cart.add(Item('Dash'));
});

Here, when we call cart.add(Item('Dash')); , it will trigger the call of the observer function to implement a mechanism that drives the event execution by the change of data.

The most traditional state management solution in Flutter applications is to use the setState method of stateful widgets. The problem exposed by this method is that the widget tree in large applications will be very complicated. Whenever the state update calls setState , it will lead to one. Start the whole body, rebuild all the subtrees, so that the performance is greatly reduced.

Then, when the ChangeNotifier observer mode is applied to the state management scheme, this problem can be solved. Imagine letting each smallest component act as an observer, observing the state of the application, and driving the local small component to update whenever the state changes. Is it possible to achieve this goal? Our common provider library applies this principle.

The provider provides a ChangeNotifierProvider widget inside, which can expose a ChangeNotifier instance (observed) to its child components:

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => CartModel(),
      child: const MyApp(),
    ),
  );
}

In the sub-components, you only need to use the Consumer widget to register the observer component to receive the CartModel internal data update of 06125c04f70904:

return Consumer<CartModel>(
  builder: (context, cart, child) {
    return Text("Total price: ${cart.totalPrice}");
  },
);

Here, the use of Consumer must specify the type of ChangeNotifier to be observed. If we want to access CartModel , we will write Consumer<CartModel> . Builder is the only required parameter of Consumer to build the sub-components displayed on the page.

The builder function will be called ChangeNotifier (In other words, when calling CartModel the notifyListeners() time method, all relevant Consumer the widget method is called a builder.), The reconstruction sub-tree, to achieve local update status object.

Navigator

Routing is a topic often discussed in Flutter applications. During the entire application process, routing operations also need to be paid attention to at all times. It is an effective way for us to understand user behavior. Flutter provides a very convenient observer mode model to help us achieve this requirement.

Each Navigator object in Flutter accepts an array of NavigatorObserver objects. In the actual development process, we can pass the navigatorObservers MaterialApp (or CupertinoPageRoute ) to the root Navigator component to observe the routing behavior of the root Navigator. The group NavigatorObserver object is a series of route observers.

 Widget build(BuildContext context) {
    return new MaterialApp(
      navigatorObservers: [new MyNavigatorObserver()],
      home: new Scaffold(
        body: new MyPage(),
      ),
    );
  }

The route observers are unified inherited from RouteObserver, the paradigm type is PageRoute, at this time, it can monitor CupertinoPageRoute and MaterialPageRoute two types of routes:

class MyRouteObserver extends RouteObserver<PageRoute<dynamic>> {

  // 监听导航器的 push 操作
  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    super.didPush(route, previousRoute);
    if (previousRoute is PageRoute && route is PageRoute) {
      print('${previousRoute.settings.name} => ${route.settings.name}');
    }
  }

  // 监听导航器的 replace 操作
  @override
  void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
    super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
    if (newRoute is PageRoute) {
      print('${oldRoute.settings.name} => ${oldRoute.settings.name}');
    }
  }

  // 监听导航器的 pop 操作
  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
    super.didPop(route, previousRoute);
    if (previousRoute is PageRoute && route is PageRoute) {
      print('${route.settings.name} => ${previousRoute.settings.name}');
    }
  }
}

We do the actual routing operations, call Navigator of pop , push when other methods, will call by convention traversal of these objects corresponding to the observer:

 Future<T> push<T extends Object>(Route<T> route) {
  // ...
  for (NavigatorObserver observer in widget.observers)
    observer.didPush(route, oldRoute);
    // ...
}

In this way, the observer mode completes this very important task in Flutter routing.

Summary of this article

This is the end of the content of this article. There are countless examples of the observer mode. In actual development, we will often need to use it, but we must remember that the application of design patterns is not to apply templates, but to be based on actual conditions. The scene finds the most suitable solution.

For the behavioral mode, the observer mode abstracts the two things of the observer and the observer, and realizes the decoupling of the code. In the actual scene, the observer may be a component that cares about a certain state and monitors a certain state. Event listeners, etc., the overall design will become more intuitive, I hope you can use it more in future development.

Further reading

About this series of articles

The Flutter / Dart design pattern from south to north (referred to as the Flutter design pattern) series of content were written by CFUG community members, author of "Flutter Development Journey from South to North", and Xiaomi engineer Yang Jiakang and published on the Flutter community official account and flutter.cn website Community tutorial section.

This series is expected to publish an article in two weeks, focusing on introducing developers to common design patterns and development methods in Flutter application development. Flutter application maintained.


Flutter
350 声望2.5k 粉丝

Flutter 为应用开发带来了革新: 只要一套代码库,即可构建、测试和发布适用于移动、Web、桌面和嵌入式平台的精美应用。Flutter 中文开发者网站 flutter.cn