Flutter with Equitable and ObjectBox: ToMany relationship not updating properly - flutter

I have the following classes:
import 'package:equatable/equatable.dart';
import 'package:objectbox/objectbox.dart';
#Entity()
/*
All fields of a class which extends Equatable should be immutable, but ObjectBox
requires the `id` field to be mutable because its value is set after an instance of
the class has been created. Because of this, we ignore the linter rule
"must_be_immutable" on all ObjectBox entities.
*/
// ignore: must_be_immutable
class Foo extends Equatable {
int id;
final String fooProp;
// I don't need a backlink yet, but very likely will in the future
// #Backlink()
// final ToMany<Bar> bars;
Foo(
this.fooProp,
{
this.id=0,
}
);
#override
List<Object> get props => [fooProp];
}
import 'package:equatable/equatable.dart';
import 'package:objectbox/objectbox.dart';
#Entity()
/*
All fields of a class which extends Equatable should be immutable, but ObjectBox
requires the `id` field to be mutable because its value is set after an instance of
the class has been created. Because of this, we ignore the linter rule
"must_be_immutable" on all ObjectBox entities.
*/
// ignore: must_be_immutable
class Bar extends Equatable {
int id;
final String barProp;
final ToMany<Foo> foos;
Bar(
this.barProp,
this.foos,
{
this.id=0,
}
);
#override
List<Object> get props => [barProp, foos];
}
And here is what I'm trying to do:
import 'package:foo_bar/objectbox/objectbox.dart';
// Get previously stored instance of Foo
Foo foo = ObjectBox.fooBox.get(1);
// Print foo.fooProp
print(foo.fooProp); // Prints "asdf"
// Change foo.fooProp to something else
foo.fooProp = 'fdsa';
// Update foo
ObjectBox.fooBox.put(foo);
// Get the same instance of Foo again
foo = ObjectBox.fooBox.get(1);
// Check foo.fooProp to make sure it updated
print(foo.fooProp); // Prints "fdsa", good so far
// Get previously stored instance of Bar which has Foo instance with ID of 1 in its foos
Bar bar = ObjectBox.barBox.get(1);
// Get our foo from bar.foos
foo = bar.foos[0];
// Verify the ID of foo to make sure it is the same object
print(foo.id); // Prints "1", exactly what we expect
// Print foo.fooProp
print(foo.fooProp); // Prints "asdf", not the expected "fdsa"
The documentation has the following to say on the subject:
Note that to-many relations are resolved lazily on first access, and then cached in the source entity inside the ToMany object. So subsequent calls to any method, like size() of the ToMany, do not query the database, even if the relation was changed elsewhere. To get the latest data fetch the source entity again or call reset() on the ToMany.
The reset() method doesn't appear to be available in the Flutter flavor of ObjectBox, and we can see from my example that even fetching both sides of the ToMany relationship did not result in the expected update.
What am I missing here?
Failed Workaround:
I tried to workaround this problem with the following awful bit of code, but even this does not work. ObjectBox just completely ignores the actual bar.foos and whatever was persisted for foos remains there and doesn't get updated.
final List<Bar> oldBars = ObjectBox.barBox.getAll();
List<Bar> newBars = [];
for(Bar oldBar in oldBars) {
if(oldBar.foos.isNotEmpty) {
List<int> oldFooIds = oldBar.foos.map((foo) => foo.id).toList();
List<Foo> newFoos = foos.where((foo) => oldFooIds.contains(foo.id)).toList();
Bar newBar = oldBar.copy(foos: ToMany<Foo>(items: newFoos));
newBars.add(newBar);
}
}
ObjectBox.barBox.putMany(newBars);
This makes me think there is something wrong with the way I have the relationship setup, but there are no errors when the ObjectBox generator runs
CALL flutter pub run build_runner build --delete-conflicting-outputs
Update:
I have this working now, but clean it is not. I had my Bar constructor set up to accept a collection of Foo objects, but passing the instances of Foo in is what was causing the relations to break. If I instead create an instance of Bar, then use bar.foos.add(foo), the result is as expected. For what it is worth, that is how the examples in the docs show interactions with relations happening, I just didn't think it was that literal, because creating new objects with relations in this manner is a hassle. I think some work can be done in the constructor to make things a bit easier still.

A "solution" (and I use that term loosely) to my problem was the following modification to the Bar class. This allows me to initialize instances of Bar with a pre-built list of instances of Foo.
import 'package:equatable/equatable.dart';
import 'package:objectbox/objectbox.dart';
#Entity()
/*
All fields of a class which extends Equatable should be immutable, but ObjectBox
requires the `id` field to be mutable because its value is set after an instance of
the class has been created. Because of this, we ignore the linter rule
"must_be_immutable" on all ObjectBox entities.
*/
// ignore: must_be_immutable
class Bar extends Equatable {
int id;
final String barProp;
final ToMany<Foo> foos = ToMany<Foo>();
Bar(
this.barProp,
{
this.id=0,
foos=const <Foo>[],
}
) {
this.foos.addAll(foos);
}
#override
List<Object> get props => [barProp, foos];
}
This works fine, for creating new objects, but because I want to use Equatable I'm must make all properties which are used to determine equality final. When a class is an #Entity which will persisted to ObjectBox, most of its properties will typically be used to determine equality, so this requirement of Equatable makes it at odds with ObjectBox when it is time to update an object. For instance, if I have an instance of Bar, I can't update barProp; I have to create a new instance of Bar which is initializes barProp to the desired value. If I create a new instance of Bar which had the desired value for barProp, foos, and has the same ID as an already persisted instance of Bar, then I try to persist that new instance, barProp will be updated as expected, but foos will not. All that to say, I first have to take the heavy handed approach of calling ObjectBox.barBox.remove() or ObjectBox.barBox.removeAll() (depending on the application) before persisting the new instance of Bar.
Foo fooA = Foo('this is Foo A');
Foo fooB = Foo('this is Foo B');
List<Foo> firstFooList = <Foo>[fooA, fooB];
ObjectBox.fooBox.putMany(firstFooList);
Foo fooC = Foo('this is Foo C')
Foo fooD = Foo('this is Foo D')
List<Foo> secondFooList = <Foo>[fooC, fooD];
ObjectBox.fooBox.putMany(secondFooList);
Bar barA = Bar('this is bar A', foos=firstFooList)
ObjectBox.barBox.put(barA); // ObjectBox assigns ID 1 to this Bar
Bar barB = Bar('this is bar B', id=barA.id, foos=secondFooList) // Use the `id` from barA which we just persisted, but initialize `foos` with `secondFooList`
// Without this, the next line which persists `barB` would result in the
// instance of Bar which has ID 1 having a `barProp` value of 'this is bar B',
// but a `foos` value equal to `firstFooList`, not the expected `secondFooList`.
ObjectBox.barBox.remove(barB.id);
ObjectBox.barBox.put(barB);
Time will tell if calling remove and removeAll like this are a bigger performance hit than not using Equatable, and for others reading this, that determination will depend on your specific application (i.e. does your app have more UI interactions that benefit from reduced builds because of the inclusion of Equatable, or does your app have more ObjectBox interactions where excessive calls to remove and removeAll cause a performance hit).

Related

NullPointerException when accessing case object in map

I am receiving a NullPointerException which I believe is due to the way objects are initialised but cannot find any supporting documentation.
I have this example code which illustrates the problem in Scala 2.12.7, I have found repeatable results in Scala 3.1.3 also:
abstract class Item(val collectionName: String)
abstract class ItemCollection(val name: String)
object TechItems extends ItemCollection("tech") {
// referencing 'name' from 'ItemCollection' superclass
case object TV extends Item(collectionName = name)
val items: Map[String, Item] = Map("tv" -> TV)
}
object Test1 extends App {
// prints 'tech'
println(TechItems.items.get("tv").map(_.collectionName))
}
object Test2 extends App {
// prints 'tech'
println(TechItems.TV.collectionName)
// throws NullPointerException
println(TechItems.items.get("tv").map(_.collectionName))
}
When running Test1, the code behaves as you'd expect. When running Test2, we now receive a NullPointerException when accessing the map after accessing the TV object directly.
When I no longer reference a field from the superclass, the issue no longer occurs:
...
object TechItems extends ItemCollection("tech") {
// using String instead of reference to superclass field
case object TV extends Item(collectionName = "mycollection")
val items: Map[String, Item] = Map("tv" -> TV)
}
...
object Test2 extends App {
// prints 'mycollection'
println(TechItems.TV.collectionName)
// prints 'Some(mycollection)'
println(TechItems.items.get("tv").map(_.collectionName))
}
My current understanding of how TechItems is initialised:
We access TechItems.TV.collectionName which begins initialising TechItems
An ItemCollection("tech") is created whose fields are then available inside of TechItems (depending on access modifiers of said superclass fields)
TV is initialised and references the superclass field name
items is initialised and references TV as a value for key "tv"
I am sure that understanding is wrong but that is what I am here to learn.
My current theory for the NullPointerException:
We access TechItems.TV.collectionName which begins initialising TechItems
items is initialised alongside TV, but items captures an uninitialised TV as null
Our access to TechItems.TV.collectionName returns the value of "tech"
TechItems.items.get("tv") returns Some(null) because TV at the point of initialising items was null, due to not being initialised.
NullPointerException is thrown
To me it feels like a somewhat farfetched theory. I am sure my lack of understanding is shown here and there is an explanation in some documentation that I have failed to find. Why do I get this NullPointerException? What is the initialisation order? And why does removing the reference to a superclass field affect this initialisation?
Wow, this is a good one!
Here is what I think is going on ...
Consider this "pseudo-java" code, that I believe more-or-less accurately reflects what is actually happening in the JVM:
class TechItems extends ItemCollection {
static MODULE = new TechItems("tech")
static class TV extends Item {
static MODULE = new TV(TechItems.MODULE.name)
}
val items = Map("tv" -> TV.MODULE)
}
So, now, when you do print(TechItems.TV.MODULE.collectionName),
TechItems.MODULE gets constructed, because we need to pull name out of it to create TV.
This constructor, runs to the Map("tv" -> TV.MODULE) line, and puts null into the map (TV.MODULE is still null - we are only figuring out what to pass to its constructor.
If you use "mycollection" instead of name, it becomes
static MODULE = new TV("mycollection"), which doesn't trigger TechItems constructor.
What happens when you don't access TV before looking at items? Well, in that case, TechItems.MODULE gets initialized first, so, by the time you get to the new TV thing, as part of constructing the items, TechItems.MODULE.name is already available, so TV.MODULE can be created and put into the map.
Dima is right. In fact, without inspecting the decompiled code, it would be harder to figure out what is happening under the hood. For simplicity, let's assume you just do these 2 calls in order (it will reproduce the issue):
println(TechItems.TV) // prints 'TV'
println(TechItems.items) // prints 'Map(tv -> null)'
Now let's decompile the code and show only the relevant parts. (I removed unnecessary code to be easier to follow) First these calls:
Predef$.MODULE$.println((Object)Main.TechItems$.TV$.MODULE$);
Predef$.MODULE$.println((Object)Main.TechItems$.MODULE$.items());
This was our Main. Now TechItems and TV:
public static class TechItems$ extends ItemCollection {
public static final TechItems$ MODULE$;
private static final Map<String, Main.Item> items;
static {
MODULE$ = new TechItems$();
items = (Map)Predef$.MODULE$.Map().apply((Seq)ScalaRunTime$.MODULE$.wrapRefArray(
(Object[])new Tuple2[] {
Predef.ArrowAssoc$.MODULE$.$minus$greater$extension(
Predef$.MODULE$.ArrowAssoc((Object)"tv"), (Object)TV$.MODULE$)
}));
}
public Map<String, Main.Item> items() {
return TechItems$.items;
}
public TechItems$() {
super("tech");
}
public static class TV$ extends Main.Item implements Product, Serializable {
public static final TV$ MODULE$;
static {
Product.$init$((Product)(MODULE$ = new TV$()));
}
public TV$() {
super(TechItems$.MODULE$.name());
}
}
When calling our first println statement we trigger the evaluation of TechItems.TV which translates to TechItems$.TV$.MODULE$. The MODULE$ is just a static final reference of TV that gets initialized in the static block of TV. To get initialized, it starts executing the static block, which in turn calls TV's constructor, new TV$() which in turn triggers the call to TechItems via: super(TechItems$.MODULE$.name());
This is the part where it gets interesting: TechItems$.MODULE$ is just the static final reference of TechItems, that was not yet referenced, so it was not yet initialized. Again, in the same manner, to get initialized, the static block of TechItems gets called. But this time the static block is different: It has to initialize TechItems$.MODULE$ and items as well, because both reside in the same static block.
Since we are in the middle of initializing TV$.MODULE$, and we just called items which requires the same reference - that we have not yet finished initializing, this reference is null at this point in time, so items is executed having TV$.MODULE$ as null.
After this, the static block of TechItems$.MODULE$ finishes, the static block of TechItems.TV finishes and we get printed TV at the console. The second print becomes self-explanatory. The call to items() returns TechItems$.items that we just evaluated in the previous call to TV, so items return Map(tv -> null) which gets printed.
Observations:
Using case object TV extends Item(collectionName = name) is precisely what triggers the issue. The logical idea is that, you do not want to evaluate items before TV finishes evaluation. So one can do 2 things: 1 - either not call TV before first calling items or just TechItems - which will trigger the evaluation of TV, and thus the correct initialization of items - or 2 (better solution) - delay evaluation of items as much as possible, until you really needed.
Naturally - the solution to the second point is to make items a lazy val. If we do this, the issue goes away, because items will no longer be evaluated unless explicitly referenced by us, and it will no longer trigger evaluation when calling just TV. And if we call items first, it will trigger TV's evaluation first. I can't show you the difference in the decompiled code because only the ScalaSignature differs: keywords like lazy are implemented as "pickled" signature bytes since these are easily picked up by the JVM through reflection.
Changing it to case object TV extends Item(collectionName = "mycollection") is also a fix. Since you no longer call super(TechItems$.MODULE$.name()); from TV at all, items's evaluation is no longer triggered when just TV is called. The call to TV's constructor becomes super("mycollection"), so the second print would then correctly evaluate items to Map(tv -> TV). This is why the null goes away when you change it.
This is an example of a circular dependency: TV "kind of" needs items and items needs TV - and the order of initialization really makes the difference between a working code and a code that throws nulls at unexpected times.
Since TV is presumably initialized lazy, making items lazy as well should theoretically remove the circular dependency.
An object definition in Scala behaves much like a lazy val with an annonymous class, that gets initialized on demand, the first time it is used.
So the first instinct when you see an object inside another object, is to assume the former object will be lazily initialized (unless explicitly referenced). Because items does reference TV explicitly, even if you don't call TV explicitly, TV will be evaluated either when referencing just TechItems or directly items, whichever comes first, because both are in the same static context, as we saw.

Why is it necessary to use constructors in dart programming language classes?

I'm a beginner learning dart from the book dart apprentice and I reached where they were discussing constructors in dart classes, the book was implying that constructors create instances of the class which I understood but I needed more info about constructors. So I googled and some results repeated what was already in the book about it being used to create instances of a class while others also showed that it's used to instantiate class properties, but my problem is with the other answer which I found that they are used to instantiate properties of a class, but my question is: I instantiate all class properties when I create the class by declaring the property variables, like this:
class UserClass{
userClassProperty = "";
anotherUserClassProperty = ""; }
why is the constructor also needed to instantiate class properties?
Often, values are unique to every class instance.
Consider the following example:
class Point {
final int x;
final int y;
const Point(this.x, this.y);
double get distanceToOrigin => sqrt(x * x + y * y);
}
If the x and y values were defined inside the class, it would be pretty useless. Instead, different Point objects can be instantiated with different values, which means the same code can be used for different situations.
Ok, so constructors instantiate or start a class by collecting all the data the class needs to start to start working. Constructors are so important that the dart compiler provides one even if you don't explicitly create one. For example, you create a class for mammals like this :
class Mammal{
String name = "cat";
int numberOfLegs = 2;
}
Although you don't explicitly add a constructor the dart compiler adds a default constructor like this :
class Mammal{
Mammal(); //This is added by dart during the class instantiation by default.
String name = "cat";
int numberOfLegs = 2;
}
Yeah, that's how crucial constructors are to the dart compiler.
And on the topic of why are they necessary even when you declare all the properties by yourself in the class, as hacker1024 said it would make the class pretty useless, as the point of the existence of classes is to create variants but with different properties. Not adding a constructor to your class and defining all the properties in the class would mean that your class doesn't take property arguments which in turn also means that different variants of your class can't be created. Again this goes directly against the point of the existence of dart classes. For example, you have a class like this :
class Mammals{
Strig name = "Human";
int numberOfLegs = 2;
bool hasFur = false;
}
final cat = Mammal();
final human = Mammal();
print(cat.numberOfLegs); //Prints 2
//2
print(human.numberOfLegs); //Also prints 2
//2
print(cat.hasFur);
// false
Yeah, this class is problematic. Cats with 2 legs? You would agree with me that that's not how things are in reality. And also the class is pretty useless in the sense that it's not modular, no matter which kind of mammal we create be it a cat, a sheep or even a cow the name property is going to be the default one we set, that is "Human". When we create a class to simulate mammals we want to be able to define what kind of properties it has, not use some fixed values. So you want to create a class which has a constructor like this :
class Mammals{
Mammals(String name,int noOfLegs, bool hasFur){
this.name = name;
this.noOfLegs = noOfLegs;
this.hasFur = hasFur;
}
String name = "";
int noOfLegs = 0;
bool hasFur = False;
}
final cat = Mammal("Cat", 4, True); //Now you can pass in the properties ou want.
final human = Mammal("Human", 2, false);
print(cat.name); //This prints the customized name of the object cat instead of some fixed value
//Cat
print(human.name); //This prints the customized name of the object human
Now we have two instances of the class with separate property values.
Although this adds a little more code, the modularity benefit is worth it.

Best practice for overriding class fields in Dart

I'm in the middle of creating a theming system for my app, where I wish to create a base theme class, with fields for colour and fonts:
abstract class BaseTheme {
late Color backgroundColor;
late Color appBarBackgroundColor;
late Color progressIndicatorColor;
late Color primaryColor;
late Color iconColor;
}
(I have to define them as late to keep the Null Safety happy! These have to be overridden anyway, so no problems there).
I would then implement this base class for each theme, as below:
import 'base_theme.dart';
class LightTheme implements BaseTheme {
Color backgroundColor = Colors.white;
Color appBarBackgroundColor = Color(0xff1a1818);
Color progressIndicatorColor = Color(0xffffc114);
Color primaryColor = Colors.red;
Color iconColor = Colors.white;
}
I would then use these values to populate the ThemeData:
BaseTheme _lightTheme = new LightTheme();
ThemeData _theme = ThemeData(
scaffoldBackgroundColor: _lightTheme.backgroundColor,
// etc...
}
Now, this appears to work fine. However, the page on overriding fields in the Linter for Dart states that overriding fields is bad practice. So, is there a better way to achieve something similar?
What's confusing is that, on that page, one example shows overriding fields when you extend a base class, which is considered bad. However, overriding fields when you implement a base class is good - But in this case, the field type has changed from Base to Derived (ie. they're not the same).
As the lint recommends, you should avoid overriding fields. Your abstract class should define an interface; as such, it should declare getters and setters and leave the implementation to the concrete, derived classes. Note that this avoids the need for late (which is an implementation detail that does not belong in the abstract base class).
abstract class BaseTheme {
Color get backgroundColor;
set backgroundColor(Color value);
Color get appBarBackgroundColor;
set appBarBackgroundColor(Color value);
// etc.
}
Dart 2.13 allows members to be declared as abstract, so you can use that to avoid the repetitiveness:
abstract class BaseTheme {
abstract Color backgroundColor;
abstract Color appBarBackgroundColor;
// etc.
}
Also see the GitHub issue: overridden_fields documentation is vague.
The reason it's not recommended as in the rule is because of memory allocation. When you override a field of an extended class you allocate more memory. For example:
class Base {
Object field = 'lorem';
Object something = 'change';
}
class Bad1 extends Base {
#override
final field = 'ipsum'; // LINT
}
In the example above when you create a new instance of Bad1 you will get field and super.field. So you allocated memory for two Object instances when you probably only needed one. You can read more about it here https://github.com/dart-lang/linter/issues/230 and here https://github.com/dart-lang/linter/issues/2428
You bring up a good point about the change in type in the docs in this example
abstract class BaseLoggingHandler {
Base transformer;
}
class LogPrintHandler implements BaseLoggingHandler {
#override
Derived transformer; // OK
}
These docs are old so this is pre null safety. Now this is a compile type error because to narrow the type you need to be more explicit and use the covaraint keyword. Like so
abstract class BaseLoggingHandler {
BaseLoggingHandler(this.transformer);
Base transformer;
}
class LogPrintHandler implements BaseLoggingHandler {
LogPrintHandler(this.transformer);
#override
covariant Derived transformer; // OK
}
Here is an example to illustrate the concept of a covaraint.
Given
abstract class Base {}
class Derived extends Base{}
the following
typedef Example = void Function(Derived);
Example e = (Base b) {};
This is valid because a Derived is a Base and if you were to give me a function (Base b) {} I can safely invoke your function passing an object of type Derived and I know operations you will take upon that object are safe.
But this is not valid for the opposite reason.
typedef Example = void Function(Base);
Example e = (Derived d) {};
Yes... Whatever you have done is correct. The link https://dart-lang.github.io/linter/lints/overridden_fields.html says its bad practice to override the class not the abstract class. When we are extending the abstract class, its compulsion to override its fields and methods. See the below screenshots.
Here are the more lint: rules https://dart.dev/tools/linter-rules

How to get properties of subclass in dart?

I have a class A with some properties:
abstract class A {
double doubleA;
String stringA;
...
A({this.doubleA = 0, this.stringA = ""});
}
and a class B with some properties, that extends class A:
class B extends A {
int intB;
String stringB;
B({
this.intB = 0,
this.stringB = "",
double doubleA = 0,
String stringA = "",
}) : super(doubleA: doubleA, stringA: stringA);
}
In my code I want to now check if an instance of A has a value that is of type of subclass B:
A a; // Value can be of different subtypes of A including B
if(a is B) {
// here dart should give me access to the properties of a like:
print(a.stringA);
// but it should also be possible to access the type B properties
// since the value of a can also be of subclass type B:
print(a.stringB);
}
This sounds wrong at first but I know that it can work because of examples in flutter.
Example Listener:
Listener(
onPointerSignal: (event) {
// event is of type PointerSignalEvent which has no property 'scrollDelta'.
// So print(event.scrollData); does not work here.
if (event is PointerScrollEvent) {
// if you check if event is of subtype PointerScrollEvent the property 'scrollDelta'
// that is included in the class PointerScrollEvent becomes available.
print(event.scrollDelta); // works without any problem.
}
},
}
However I have not been able to replicate this with my classes A and B and I don't know why it doesn't work. I have also looked into the implementations of these flutter classes and copied the class structure but I can still only access the properties of A after the check if(a is B) which doesn't correspond to the behavior observed with the flutter classes.
What am I doing wrong? Am I am missing something?
Thanks for reading :D <3
If you declare a variable 'a' as being of type 'A', you will not be able to access the properties of classes inheriting from A (like B).
Let's say A is Animal and B is Baboon. Baboon inherits properties from Animal, so all variables instantiated with the Baboon type with have access to properties from both classes. But variables instantiated with the Animal type will only have access to the Animal properties.
Here are some examples: https://medium.com/jay-tillu/inheritance-in-dart-bd0895883265

How to observe field variable of other class in Mobx flutter?

I am using the flutter Mobx for state management.
I have a simple class:-
class A {
int x;
A(this.x);
}
How can I observe if x changes inside the class in another Mobx store:-
class MyStore extends _MyStore with _$MyStore {
Subs(A a) : super(a);
}
abstract class _MyStore with Store {
#observable
A a;
_Subs(this.a)
}
I want MyStore to observe the a.x.
Is it possible, if yes how?
I ran in to the same issue the other day using flutter mobx ^1.2.1+3 (dart) and
flutter_mobx ^1.1.0+2.
The first thing that comes to my mind is to annotate the field in question, I.e x with the #observable attribute. But it doesn't seem to be effective outside a store class.
So you have to observe the field using the Observable class.
To make it work your code should look something like this:
class A {
//replace with less verbose "var"
Observable<int> x = Observable(0);
A(this.x);
}
class MyStore extends _MyStore with _$MyStore {
Subs(A a) : super(a);
}
abstract class _MyStore with Store {
A a;
_Subs(this.a)
//Will be calculated whenever a change to a.x is observed.
#computed
int get xSquare => a.x.value * a.x.value;
}
As you can see I removed the observable attribute from a, since it does not need to be observed if you want to react to changes to a.x in your store. You probably noticed that you have to access the value of the observable using .value.
That should conclude how you observe a field of a class external to the store, inside your store.
I am not sure that this would be helpful since it is Javascript/Typescript, but that's what I would do :
class Foo {
#observable name = 'foo'
}
class Bar {
foo: Foo
constructor(instanceOfFoo) {
this.foo = instanceOfFoo
autorun(() => {
// Logs foo name when it changes
console.log(this.foo.name)
})
reaction(
() => this.foo.name,
() => {
// Logs foo name when it changes
console.log(this.foo.name)
}
)
}
#observable
name = 'bar'
#computed
get fooNamePlusBarName {
// recomputes automatically whenever foo or bar name changes
return this.foo.name + this.name
}
}
Basically you pass Foo instance to the Bar constructor (or just use imported singleton if it fits you), then you have 3 options: computed, reaction and autorun