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

Migrate from provider to context_plus #12

Open passsy opened 2 months ago

passsy commented 2 months ago

I'd like to have a quick small guide how to migrate from provider to context_plus.

Basic usage works, but I'd like to know if I can migrate more advanced topics like ProxyProvider or sub scopes.

s0nerik commented 2 months ago

I've almost finished the site with various usage examples. Hopefully, I'll publish it towards the end of the next week. Will let you know.

Regarding the migration, there won't be 1:1 correspondence, but I believe context_plus supports the same amount of usecases as Provider, or maybe even more.

You can:

Also, Ref<AnimationController> and a couple of other types of refs provide an additional vsync parameter so that you can create and bind them easily as well.

The whole API is intentionally kept minimal-ish:

Ref.bind() - create eagerly, then bind
Ref.bindLazy() - bind, then create lazily upon first request
Ref.bindValue() - just bind
(Stream/Future/Listenable/ValueListenable/AsyncListenable/Ref<Stream>/Ref<Future>/Ref<Listenable>/Ref<ValueListenable>/Ref<AsyncListenable>/...).watch() - rebuild on any notification
(Stream/Future/Listenable/ValueListenable/AsyncListenable/Ref<Stream>/Ref<Future>/Ref<Listenable>/Ref<ValueListenable>/Ref<AsyncListenable>/...).watchOnly() - rebuild only when selected value changes upon notification

Hope that helps. Stay tuned for the site, I love how it pans out so far.

yousefak007 commented 2 weeks ago

Hi, @s0nerik @jinyus @passsy,

I think introducing a bindProxy method, similar to the ProxyProvider would be more convincing and eliminate the need for users to manually manage keys.

Old Way:

final value1 = value1Listenable.watch(context);
final value2 = value2Listenable.watch(context);
final value3 = _value3Ref.bind(
  context,
  () => Value3(value1, value2),
  key: (value1, value2),
);

New Way:


// case 1
final value = _value3Ref.bindProxy(
  context,
  (context, T? previous) {
    final value1 = value1Listenable.watch(context);
    final value2 = value2Listenable.watch(context);
    return Value3(value1, value2);
  },
);

// case 2
// or we can use the previous value without the need to create a new one
final value = _value3Ref.bindProxy(
  context,
  (context, T? previous) {
    final value1 = value1Listenable.watch(context);
    final value2 = value2Listenable.watch(context);

    if (previous != null) {
      previous.valueOne = value1;
      previous.valueTwo = value1 + value2;
      return previous;
    }

    return Value3(value1, value2);
  },
);

In this new approach, keys are managed internally, simplifying the API for users.

Considerations:

For Case 2, where we want to reuse the previous object if possible to avoid disposing of it, I see two potential solutions:

  1. Add a property autoDisposePrevious to bindProxy, allowing users to control whether the previous instance should be automatically disposed of or retained.

  2. Compare the hashcode of the returned object with the previous object. If they match, there's no need to dispose of the previous object. However, care must be taken when dealing with records, as comparing them by reference can be tricky.

s0nerik commented 2 weeks ago

@yousefak007 The examples you provide in support of the .bindProxy() are, IMO, easier to represent without it:

// case 1 (When `Value3` is a data object)
final value1 = value1Listenable.watch(context);
final value2 = value2Listenable.watch(context);
final value = _value3Ref.bindValue(context, Value3(value1, value2));
// case 1 (When `Value3` is a controller)
final value1 = value1Listenable.watch(context);
final value2 = value2Listenable.watch(context);
final value = _value3Ref.bind(
  context,
  () => Value3(value1, value2),
  key: (value1, value2),
);
// case 2
final value1 = value1Listenable.watch(context);
final value2 = value2Listenable.watch(context);
final value = _value3Ref.bind(context, () => Value3(value1, value2))
    ..valueOne = value1
    ..valueTwo = value1 + value2;

2/3 cases can avoid using keys, and the one that requires a key seems like a better approach to me than trying to force all controller types to have a meaningful equals/hashCode pair.

UPD. On the other hand, though, tinkering with a BuildContext provided to .bindProxy() and comparing the returned values by the identity might just work... I'll think about it for some time, will be glad to hear what you guys think.

yousefak007 commented 2 weeks ago

@s0nerik I came up with this solution, take a look: and I think it's more composable, and easy to do, rather than remembering to put keys the keys hell make more bugs if you forget to put it like Reactjs

// we need to define it in somewhere shared for all libraries
final _keys = [];

// in context_watch:
// inside watch method
T watch(BuildContext context) {
  InheritedContextWatch.of(context)
      .getOrCreateObservable(context, this)
      ?.watch();
  // just add the value to build keys
  _keys.add(value);
  return value;
}

// in context_ref:
// inside bind method
ValueProvider<T> bind<T>({
  required BuildContext context,
  required Ref<T> ref,
  required T Function() create,
  required void Function(T value)? dispose,
  required Object? key,
}) {
  assert(context is Element);

  ....

  final provider = ref.getOrCreateProvider(context);
  // we need to make disposer before the key
  provider.disposer = dispose;
  provider.key = key;

  ....
}

// Impl. of Proxy method

T bindProxy(
  BuildContext context,
  T Function(BuildContext context, T? previous) create, {
  void Function(T value)? dispose,
}) {

  final previousValue = this._providers[context]?.value;
  final value = create(context, previousValue);

  void Function(T value)? disposeFn = dispose;

  // in somehow figure how is previous used or not

  // solution 1:
  // add {bool autDisposePrevious = true} to parameters
  // then use condition to dispose or not
  if (autDisposePrevious == false) {
    // this will prevent the dispose from working
    disposeFn = (_) {};
  }

  // solution 2:
  // compare between the hashcode of previous value and the current value
  // then dispose if there is difference between references
  // but should find away if the value is record
  if (value.hashCode == previousValue.hashCode) {
    // this will prevent the dispose from working
    disposeFn = (_) {};
  }

  final result = bind(
    context,
    () => value,
    dispose: disposeFn,
    key: _keys,
  );
  _keys.clear();

  return result;
}
yousefak007 commented 2 weeks ago

@s0nerik I think the proxy method will have 3 main features:

1- have direct access to the previous value 2- have the ability to update the previous value without the need to create a new instance (so element tasks like initializations or maybe make some HTTP requests once every change) 3- a better way to manage keys rather than depending on adding them manually

in my opinion, this library will be replacing Provider and it solves the Provider missing features, so we need this library to cover the features that Providers can do

s0nerik commented 2 weeks ago

@yousefak007 I've created a separate issue to track the potential Ref.bindProxy() implementation. Feel free to comment there: https://github.com/s0nerik/context_plus/issues/16