s0nerik / context_plus

Convenient BuildContext-based value propagation and observing. Seamlessly integrates with Flutter's built-in observability primitives.
MIT License
32 stars 4 forks source link

How to perform side effects? #9

Closed Luckey-Elijah closed 3 weeks ago

Luckey-Elijah commented 4 months ago

I was looking into this package an curious how to perform side effect such as navigation with context_plus. I couldn't find much information on this on pub.dev or the docs

s0nerik commented 4 months ago

Currently, context_plus doesn't provide a way to add a listener inside the build method that would just fire the callback without causing the BuildContext to rebuild.

The biggest reason why there's no such API yet is because I want the context_plus to not only be convenient, but also correct. Just adding a .listen(context, (value) => <callback>) will lead to potentially incorrect behavior if it's used conditionally since it's impossible to know which callback belongs to which code branch:

if (something) {
  observable.listen(context, (value) => print('listener1'));
} else {
  observable.listen(context, (value) => print('listener2'));
}

I haven't come up with an alternative approach that maintains both correctness and conciseness yet.

Luckey-Elijah commented 4 months ago

Okay, thank you very much for clarifying!

s0nerik commented 1 month ago

Here's what I'm doing currently to achieve what you were looking for:

final _listener = Ref<VoidCallback>();
...
_listener.bind(
  context,
  () {
    void listener() {
      print('Hello, callback!');
    }

    listenable.addListener(listener);
    return listener;
  },
  dispose: listenable.removeListener,
  key: listenable,
);

Looks quite massive, but does the job.

I'm considering options to make this more convenient. It looks possible to condense this into something like

final _listener = ListenerRef<AnyObservable>(); // or Ref<Listener<AnyObservable>>()
...
_listener.bind(
  context,
  observable,
  () {
    print('Hello, callback!');
  },
);

I'm still yet to decide which syntax to choose for this, maybe I'll start a poll somewhere soon. :D Stay tuned!

s0nerik commented 1 month ago

After playing around with various options for implementing side effects, I suddenly realized that it's already there in an easy-to-use form. All you need is a .watchOnly() call that always returns null.

So, to run a side effect on each notification from the observable without causing the widget to rebuild you can do the following:

observable.watchOnly(context, () {
  print('Hello, callback!');
  return null;
});

That's it! As simple as that. Moreover, thanks to the behavioral rules of .watch() and .watchOnly() calls, the callback is replaceable via hot reload and should properly unregister as soon as the .watchOnly() call disappears from the build method automatically.

I'll add that to the examples soon.

UPD.

Sorry for the misguidance, late-night thinking provoked a rushed conclusion.

Unfortunately, watchOnly() approach to side effects is not universal. It worked for me because I had all the side effects under a condition, like this:

observable.watchOnly(context, () {
  if (!observable.didPrintHello) {
    print('Hello, callback!');
    observable.didPrintHello = true;
  }
  return null;
});

If I were to change this to just

observable.watchOnly(context, () {
  print('Hello, callback!');
  return null;
});

the print would've happened for each widget build, which is not what most package users would want.

So, side effects require a separate API, and I keep thinking about the available options.

Luckey-Elijah commented 1 month ago

Wow! Surprisingly elegant. Thank you for spending the time investigating this

s0nerik commented 1 month ago

@Luckey-Elijah sorry for the misguidance, please see an update on the comment above.

s0nerik commented 3 weeks ago

context_plus 4.0 and context_watch 5.0 now provide .watchEffect() for all supported observable types.