Hi, we’ll look into how we can write our own observables in dart today.

Observables, as the name suggests are the variables we can observe for changes in our code, at times we need to run a block of code when a value of a variable changes, checking for changes in every frame becomes cumbersome, that is where a observable library comes in handy.

Go ahead and create dart project

dart create ./observable

You’ll end up having observables.dart inside the bin folder of the new project, we’ll be using this file for testing ( we could have a created a package project but we are taking the easier route)

Create folder called src at the bin folder and create a observable_base.dart inside th src folder

-bin/
  |-src/
  |  |-observable_base.dart
  |-observable.dart

Open up the observables_base.dart Create a class named Observable

class Observable{
  
}

add field called value, this is the value that we are going to obesrve in our observables

class Observable {
  dynamic value;
}

lets make this observable be used with any dart type

//this is a valid dart code
class Observable<type_of_the_value> {
  type_of_the_value value;
}

is same as

//this is a valid dart code
class Observable<T> {
  T value;
}

this makes our Observable be used with any dart type, if we need the observable value of type int we can go ahead and create a observable like this

final myIntObs = Observable<int>();
///set the value 
myIntObs.value = 1;

but as of now our code isn’t nullsafe, if your dart sdk has null safety enabled the observable class would be asking you to initialize the value, we’ll do that

class Observable<T> {
  T value;
  Observable(this.value);
}

now when we create observable we need to initialize its value, that means if you need to initialize the observable with null just put a ? operator after the type and pass null to the observable constructor

final myIntObs = Observable<int?>(null);
//myIntObs.value is a nullable type 
final myIntObs2 = Observable<int>(1);
//myIntObs2.value is not a nullable type

now we’ll need a [1] getter and a [2] setter for the observable to get and set the value (obviously), make [3] the existing value variable a private variable by appending a _ to it as we’ll access the value by getter and setter

class Observable<T>{
  ///3]
  T _value;
  ///constructor with default value
  Observable(this._value);

  //[1]
  T get value{
    return _value;
  }
  //[2]
  set value(T newValue){
    _value = newValue;
  }
}

now we can get and set the value like this

final myIntObs = Observable<int>(1);
print(myIntObs.value); ///prints 1
myIntObs.value = 2;
print(myIntObs.value); ///prints 2

How do we listen to changes/updates of the observables? at a given time 0 to n number of listeners can listen to a observable change, [1] we’ll need to create a object to hold listener information and it’ll be easier to release them from memory when they are of no use, we’ll name the object ObservableListener, [2] it’ll have _onChange which will be the listener function the user passes and [3] _observable which will be the participating observable

//[1]
class ObservableListener{  
  //[2]
  final void Function() _onChange;
  //[3]
  final Observable _observable;

  //constructor
  ObservableListener(
    this._observable,
    this._onChange,
  );
}

[1] as 0 to n number of listeners can listen to a observable at a given time, to hold them we’ll add List<ObservableListener> to Observable

class Observable<T>{
  ///value the observable is holding
  T _value;
  //[1]
  List<ObservableListener> _listeners = [];
  ///constructor with default value
  Observable(this._value);

  ///get the current holding value
  T get value{
    return _value;
  }
  ///set new value
  set value(T newValue){
    _value = newValue;
  }
}

[1] now create a method named listen which takes a function as a parameter inside the Observable class, this function will be called everytime the value of Observable changes (not yet),

[2] when listen is called we will return ObservableListener so the caller is responsible for disposing/stopping the observing for the changes (subscription!!), in body of the listen function we’ll instatiate a ObservableListener by passing this and the function parameter of the listen method, we are passing this as the observable to the ObservableListener because the observable involved is the one that listen is being called on, [3] we’ll add the instatiated ObservableListener to our _listeners list in the observable so we have a reference to the currently active listeners, and we’ll return the same from our listen method so the end user also has the reference.

class Observable<T>{
  ///value the observable is holding
  T _value;
  ///all the listeners listening to this observable currently
  List<ObservableListener> _listeners = [];
  ///constructor with default value
  Observable(this._value);

  //[1]
  ObservableListener listen(Function() fn) {
    final newListener = ObservableListener(this, fn);
    //[3]
    _listeners.add(newListener);
    //[2]
    return newListener;
  }

  ///get the current holding value
  T get value{
    return _value;
  }
  ///set new value
  set value(T newValue){
    _value = newValue;
  }
}

[1] ObservableListener needs a method to stop listening, that means all we need to do is remove the listener from th _listeners list, but the end user will not have reference to _listeners, but they would have recieved a reference to ObservableListener when they called listen, lets create a stopListening function, [2] which will call _removeListener on the Observable which in turn calls remove on the _listeners list

class ObservableListener {
  ///the function which should be run when the participating observable changes
  final void Function() _onChange;

  ///the participating observable
  final Observable _observable;

  //constructor
  ObservableListener(
    this._observable,
    this._onChange,
  );
  //[1]
  void stopListening() {
    _observable._removeListener(this);
  }
}

class Observable<T>{
  ///value the observable is holding
  T _value;
  ///all the listeners listening to this observable currently
  List<ObservableListener> _listeners = [];
  ///constructor with default value
  Observable(this._value);

  ///add a listener function to this observable, we'll return a reference so user can dispose/stop listening
  ObservableListener listen(Function() fn) {
    final newListener = ObservableListener(this, fn);
    _listeners.add(newListener);
    return newListener;
  }

  //[2]
  void _removeListener(ObservableListener listener) {
    _listeners.remove(listener);
  }

  ///get the current holding value
  T get value{
    return _value;
  }
  ///set new value
  set value(T newValue){
    _value = newValue;
  }
}

we are now able to add listeners to Observable but we are no recieving any updates on the listener function we passed when we called listen

[1] for that to happen we’ll need to be informed when a value is read and set, so we’ll ad another two methods name _informRead and _informWrite to the observable, and we’ll call them from value getter and setter respectively

class Observable<T>{
  ///value the observable is holding
  T _value;
  ///all the listeners listening to this observable currently
  List<ObservableListener> _listeners = [];
  ///constructor with default value
  Observable(this._value);

  ///add a listener function to this observable, we'll return a reference so user can dispose/stop listening
  ObservableListener listen(Function() fn) {
    final newListener = ObservableListener(this, fn);
    _listeners.add(newListener);
    return newListener;
  }

  ///get the current holding value
  T get value{
    _informRead();
    return _value;
  }
  ///set new value
  set value(T newValue){
    _value = newValue;
    _informWrite();
  }

  //[1]
  void _informWrite() {
  }

  //[1]
  void _informRead() {
  }
}

[1] add a global variable called _observablesReadInLastFrame which will be List<Observable>,

[2] we will populate _observablesReadInLastFrame from _informRead with calling observable, we’ll use this to automatically fetch all the Observables involved/used in listener function, and run the function everytime any one those observable change, so whenever a value of observable is read we’ll add that observable to _observablesReadInLastFrame (more details ahead)

[3] everytime _informWrite is called we’ll inform all the listener in the _listeners list by calling _onChange in the listener so every listeners gets informed. and

//[1]
final _observablesReadInLastFrame = <Observable>[];

class Observable<T>{
  ///value the observable is holding
  T _value;
  ///all the listeners listening to this observable currently
  List<ObservableListener> _listeners = [];
  ///constructor with default value
  Observable(this._value);

  ///add a listener function to this observable, we'll return a reference so user can dispose/stop listening
  ObservableListener listen(Function() fn) {
    final newListener = ObservableListener(this, fn);
    _listeners.add(newListener);
    return newListener;
  }

  ///get the currently holding value
  T get value{
    _informRead();
    return _value;
  }

  ///set new value
  set value(T newValue){
    _value = newValue;
    _informWrite();
  }

  //[3]
  void _informWrite() {
    _listeners.forEach((element) {
      element._onChange();
    });
  }
  
  //[2]
  void _informRead() {
    _observablesReadInLastFrame.add(this);
  }
}

now most part of our observable system is ready, but imagine a function which will take a Function as a parameter and runs it everytime any observable inside parameter function updates, lets name it runAndRunEverytimeTheInvolvedObservableChanges

runAndRunEverytimeTheInvolvedObservableChanges(Function() parameter){
  
}

now imagine a scenerio where we pass a function and this happens

final myIntObs = Observable<int>(1);
runAndRunEverytimeTheInvolvedObservableChanges(()=>print(myIntObs.value)));//prints 1 once
//update the observable
myIntObs.value = 2;
//runAndRunEverytimeTheInvolvedObservableChanges again prints 2 due to update

for this imaginative function to take life, lets go ahead create a global method/function and add the body,

[1] before calling the passed parameter clear the _observablesReadInLastFrame, so we can get all observables involved in the passed function parameter,

[2] call the passed function parameter, which would populae the _observablesReadInLastFrame (remeber _informRead)

[3] now get all the observables involved in the function parameter by copying the _observablesReadInLastFrame list and clear them for next use, if the observablesInvolved is empty list throw a error that no observables has been used in the function

[4] now as we have the list of observable in the function, lets listen each one of them to run the function parameter everytime any one of them changes which will result in multiple ObservableListener (List<ObservableListener>)

[5] lets create object called MultiObservableListener to contain all this in a single object as List<ObservableListener> _listeners

[6] add a stopListening which will further call the stopListening to each listeners in _listeners list effectively stopping all updates for runAndRunEverytimeTheInvolvedObservableChanges

[7] return the MultiObservableListener from runAndRunEverytimeTheInvolvedObservableChanges so the caller will have a refernce to stop this


runAndRunEverytimeTheInvolvedObservableChanges(Function() fn){
  //[1]
  _observablesReadInLastFrame.clear();
  //[2]
  fn();
  //[3]
  final observablesInvolved = _observablesReadInLastFrame.toList();
  _observablesReadInLastFrame.clear();
  if(observablesInvolved.isEmpty){
    throw Exception("No observables detected in passed function");
  }
  //[4]
  final allListeners = observablesInvolved.map((e)=>e.listen(fn)).toList();
  //[7]
  return MultiObservableListener(allListeners);
}

//[5]
class MultiObservableListener {
  final List<ObservableListener> _listeners;

  MultiObservableListener(this._listeners);
  //[6]
  void stopListening() {
    _listeners.forEach((element) {
      element.stopListening();
    });
  }
}

Now we have a small observable library which can listen for changes of any type of object (no list,set,map support yet, we’ll see them in the next chapter)

complete code observable_base.dart

library observables;

List<Observable> _observablesReadInLastFrame = [];

///runs the passed function once finds the observables involved, listens for changes of those
///observables runs the passed function again when there is update for any involved observables,
///if no observable is detected an exception is thrown
MultiObservableListener runOnceAndRunForEveryChange(void Function() fn) {
  _observablesReadInLastFrame.clear();
  fn();
  final observablesinvolved = _observablesReadInLastFrame.toList();
  _observablesReadInLastFrame.clear();
  if (observablesinvolved.isEmpty) {
    throw Exception("No observables detected");
  }
  return MultiObservableListener(
    observablesinvolved.map((e) => e.listen(fn)).toList(),
  );
}

///onject holding reference to multiple listeners involved in [runOnceAndRunForEveryChange]
class MultiObservableListener {
  final List<ObservableListener> _listeners;

  MultiObservableListener(this._listeners);

  ///stops listening for every observable change involved in [runOnceAndRunForEveryChange]
  void stopListening() {
    _listeners.forEach((element) {
      element.stopListening();
    });
  }
}

///object holding refernce to listening of aobservable
class ObservableListener {
  ///the function which should be run when the participating observable changes
  final void Function() _onChange;

  ///the participating observable
  final Observable _observable;

  //constructor
  ObservableListener(
    this._observable,
    this._onChange,
  );

  ///stop listening for updates
  void stopListening() {
    _observable._removeListener(this);
  }
}

class Observable<T> {
  ///value the observable is holding
  T _value;

  ///all the listeners listening to this observable currently
  List<ObservableListener> _listeners = [];

  ///constructor with default value
  Observable(this._value);

  ///add a listener function to this observable, we'll return a reference so user can dispose/stop listening
  ObservableListener listen(Function() fn) {
    final newListener = ObservableListener(this, fn);
    _listeners.add(newListener);
    return newListener;
  }

  ///removes the given listener from list [_listeners]
  void _removeListener(ObservableListener listener) {
    _listeners.remove(listener);
  }

  ///get current value of the observable
  T get value {
    _informRead();
    return _value;
  }

  ///set the current value of the observable
  set value(T value) {
    _value = value;
    _informWrite();
  }

  void _informRead() {
    _observablesReadInLastFrame.add(this);
  }

  void _informWrite() {
    _listeners.forEach((element) {
      element._onChange();
    });
  }
}

go ahead and populate our main in observable.dart with fallowing code and run it to test our small library

import 'src/observable_base.dart';

void main(List<String> arguments) {
  final obs = Observable<int>(0);
  final obs2 = Observable("yes");
  final oW = runOnceAndRunForEveryChange(() {
    print(obs.value);
    print(obs2.value);
  });
  print("updating while runOnceAndRunForEveryChange is active");
  obs.value = 1;
  obs2.value = "no";
  oW.stopListening();
  print("updating while runOnceAndRunForEveryChange is inactive");
  obs.value = 3;
  print("program exited");
}

which should give you the expected result

0
yes
updating while runOnceAndRunForEveryChange is active
1
yes
1
no
updating while runOnceAndRunForEveryChange is inactive
program exited

thats all folks.