Pub.dev package GitHub repository

Observer Pattern - Subject

Observer Pattern implementation for Dart, using callbacks, streams and states. Subject code generator with annotations, to automatically generate an observable interface for any class.

Click here to see how to setup the Code Generation.

Features

  • Subject and Observer, as base implementation
  • Callback, Stream, Sink and Stateful mixins, to extend the Subject and Observer classes
  • Alternative implementations, such as Publisher and EventEmitter
  • Code generation using @subject and @observe, to automatically generate an observable interface for any class

Getting Started

dart pub add subject

And import the package:

import 'package:subject/subject.dart';

Usage

Create a subject by initializing or extending Subject, you can set the type of the state that will be passed to the observers:

final subject = Subject<String>();

Create an observer and attach it to the subject. An observer can be created using Observer or the alternative implementations:

final observer = Observer<String>(
  (subject, state) => print('Observer Callback: $state')
);

subject.attach(observer);

Notify the subject to update the state and notify the observers:

subject.notify('Hello World!');

Subjects and Observers

You have different bases to create a subject and observer, each with its features:

Callback - Observer

final subject = Subject<String>();
final observer = Observer<String>((subject, state) => print('Observer: $state'));

subject.attach(observer);
subject.notify('Hello World!');

Async - Subject.sink / Observer.stream

final subject = Subject.sink<String>(sync: true);
final observer = Observer.stream<String>(sync: true);

observer.listen((state) => print('Observer: $state'));

subject.attach(observer);
subject.add('Hello World!');

Coupled - Observer.coupled

final subject = Subject<String>();
final observer = Observer.coupled<String>(
  attached: (subject, observer) => print('Observer Attached'),
  detached: (subject, observer) => print('Observer Detached'),
);

subject.attach(observer);
subject.notify('Hello World!');

Stateful - Subject.stateful / Observer.stateful

final subject = Subject.stateful<String>(state: 'Initial State', notifyOnAttach: true);
final observer = Observer.stateful<String>();

subject.attach(observer);
subject.notify('Hello World!');

print('Subject State: ${subject.state}');

Mixins

You can create your own subject and observer classes, by extending the base classes and mixing the desired features:

Subject

  • StreamableSubject - Transforms the subject into a stream, subscriptions are attached to the subject as auto disposable StreamObservers
  • SinkableSubject - Transforms the subject into a sink, which notifies the subject when a value is added
  • SubjectState - Allows the subject to have a persistent state

Observer

  • Cancelable - Allows the observer to be canceled, which will detach it from the subject
  • Callbackable - Allows the observer to be instantiated with a callback
  • StreamableObserver - Transforms the observer into a stream, which can be listened to
  • ObserverState - Allows the observer to have a persistent state

Code Generation

With the @subject and @observe annotations, you can generate an observable interface for any class automatically. You can listen to methods calls, and changes in properties, using the .on() and .onBefore() methods.

Use the following commands to continuously generate the code:

dart pub add build_runner -d
dart run build_runner watch -d

Or, using Flutter:

flutter pub add build_runner -d
flutter pub run build_runner watch -d

These commands will add the build_runner package as a development dependency and run it with the watch -d command, or build -d for only once. You only need to add it one time.

Annotation @subject

By using the @subject annotation you can generate a subject class for the annotated class. The generated class will be named ${className}Subject, and a mixin named Observable${className}.

@subject
class User {
  final String name;

  User(this.name);

  void say(String message) => print('$name says "$message"');
}

The @subject annotation will create a UserSubject class that wraps all methods and properties in a notify call, making them observable.

You can use the @dontObserve annotation to exclude elements from the generated subject class.

Annotation @observe

The @observe annotation is used to indicate which methods and properties should be observable in the generated subject class.

class User {
  final String name;

  User(this.name);

  @observe
  void say(String message) => print('$name says "$message"');
}

In the User class, only the say method is annotated with @observe, which means it will be the only observable element in the generated UserSubject class. The other elements of the class will not be observable.

The @observe annotation overrides the @subject annotation, so if you use both, only the elements annotated with @observe will be observable.

Listening to events

To listen to events, you can use the .on() and .onBefore() methods, which are included in the generated subject class. The .on() method contains all the generated methods and setters, making it easy to listen to events for the annotated class.

final user = UserSubject('John');

user.on(
  say: (message) => print('User said "$message"'),
);

Example

Subject / Observer (View on GitHub)
import 'package:subject/observer.dart';

void main() {
final subject = Subject<String>();

final observer = Observer<String>((subject, state) => print('Observer 1: $state'));
final streamObserver = Observer.stream<String>()..listen((state) => print('Observer 2: $state'));

subject.attach(observer);
subject.attach(streamObserver);

subject.notify('Hello World!');
print('There are ${subject.observers.length} observers attached to the subject.');

print('Detaching observer...');
subject.detach(observer);

subject.notify('Hello World, again!');

/* [Output]
  Observer 1: Hello World!
  Observer 2: Hello World!

  There are 2 observers attached to the subject.
  Detaching observer...

  Observer 2: Hello World, again!
*/
}
Code Generator (View on GitHub)
import 'package:subject/subject.dart';

part 'build.g.dart';

@subject
class User<T> {
final String name;
String? thought;

@dontObserve
T value;

User(this.name, this.value);

// @observe
void say(String message) => print('$name says "$message"');
}

void main() {
final user = UserSubject('John', 4);

user.on(
  say: (message) => print('User said "$message"'),
  thought: (value, previous) => print('User thought "$value"'),
);

user.say('Hello World!');
user.thought = 'I am thinking...';

/* [Output]
  John says "Hello World!"
  User said "Hello World!"
  User thought "I am thinking..."
*/
}
Extending (View on GitHub)
import 'package:subject/observer.dart';

class User extends Subject<String> {
final String name;

User(this.name);

void say(String message) {
  print(message);
  notify(message);
}
}

class UserObserver with Observer<String> {
@override
void update(Subject<String> subject, String message) {
  if (subject is! User) return;
  print('${ subject.name } says "$message"');
}
}

void main() {
final user = User('John');
user.attach(UserObserver());

user.say('Hello World!');

/* [Output]
  Hello World!
  John says "Hello World!"
*/
}
Sink / Stream (View on GitHub)
import 'package:subject/observer.dart';

void main() {
final subject = Subject.sink<String>();

final observer = Observer.stream<String>();
observer.listen((message) => print('Observer: "$message"'));

subject.attach(observer);
subject.add('Hello World!');

/* [Output]
  Observer: "Hello World!"
*/
}
Stateful (View on GitHub)
import 'package:subject/observer.dart';

/* -= Stateful - Subject =- */

void statefulSubject() {
final subject = Subject.stateful<String>(notifyOnAttach: true);

subject.notify('Hello World!');
subject.attach(Observer((subject, state) => print('Observer: "$state"')));

print('The state is "${ subject.state }"');

/* [Output]
  Observer: "Hello World!"
  The state is "Hello World!"
*/
}

/* -= Stateful - Observer =- */

void statefulObserver() {
final subject = Subject<String>();

final observer = Observer.stateful<String>();
subject.attach(observer);

subject.notify('Hello World!');
print('The state is "${ observer.state }"');

/* [Output]
  The state is "Hello World!"
*/
}

void main() {
print('[Stateful Subject]');
statefulSubject();

print('');
print('[Stateful Observer]');
statefulObserver();
}
Publisher (View on GitHub)
import 'package:subject/publisher.dart';

void main() {
final publisher = Publisher<String>();

final subscriber = Subscriber<String>((subject, message) => print('Callback: "$message"'));
publisher.subscribe(subscriber);
publisher.subscribe(Subscriber<String>()..listen((message) => print('Stream: "$message"')));

publisher.publish('Hello World!');

print('There are ${ publisher.subscribers.length } subscribers attached to the publisher.');

subscriber.cancel();
publisher.publish('Hello World, again!');

/* [Output]
  Callback: "Hello World!"
  Stream: "Hello World!"

  There are 2 subscribers attached to the publisher.

  Stream: "Hello World, again!"
*/
}
EventEmitter (View on GitHub)
import 'package:subject/event_emitter.dart';

void main() {
final events = EventEmitter();

final listener = events.on('message', (String data) => print('String: $data'));
events.on('message', (int data) => print('Integer: $data'));

events.emit('message', 'Hello World!');
events.emit('message', 42);

listener.cancel();
events.emit('message', 'Hello World, again!');

// [Output]
// String: Hello World!
// Integer: 42
}

Contributing

Contributions are welcome! Please open an issue or pull request if you find a bug or have a feature request.