Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Selective state recreation #764

Closed
clragon opened this issue Jul 10, 2022 · 7 comments
Closed

Selective state recreation #764

clragon opened this issue Jul 10, 2022 · 7 comments
Labels
enhancement New feature or request needs triage

Comments

@clragon
Copy link

clragon commented Jul 10, 2022

Is your feature request related to a problem? Please describe.
Some of the objects I want to provide can hold state and also depend on other providers above them.
Provider offers two ways of handling this;
Either I use the Provider.update function to recreate my object with the new state of the other providers,
or I use a ChangeNotifier which then contains a method to update the object with the new parent states.

Since my objects hold state themselves, recreating them in the update function of a ProxyProvider without any conditions cannot work, because it leads to over-recreation. Any rebuild will recreate them, which is undesirable and I lose state.
This means I would have to fill the update function with If statements that only recreate said object if the parents changed, however, this would require state, so I have to make my widget stateful and store the last version of said parents, or something similar to that.
this does not seem very nice to me.

If my objects are ChangeNotifiers then I can use a ChangeNotifierProxyProvider, but ChangeNotifierProxyProvider is setup in a way that does not allow passing the dependencies into the object via the constructor.
If my object is a ChangeNotifier, I now have to create a new method to sideload my parent state,
which means my object can temporarily be in an "invalid" state, where the parent dependencies are not available yet.
This does not work well with objects that could be used without a Provider, where the parent dependencies are required.
If I do not want to change my objects, then I have to wrap them with a ChangeNotifier that then recreates them internally.
if they are themselves ChangeNotifiers then I now have a weird construct that wraps a ChangeNotifier with a ChangeNotifier so that I can replace the inner ChangeNotifier. it also means that I now have to unpack my inner ChangeNotifier everytime I grab it from context. This also does not seem appealing to me.

Describe the solution you'd like
It would be nice if there was some easy way of having my providers only recreate their value when their dependencies change, kind of like a select but for provider values.

I am not sure if I just missed something or if the thing that I am attempting would be bad practice or abuse of the way Provider works.

Regardless, it is actually possible to kind of add this functionality in a reusable way from the outside, with a mixin as I have done here:
https://gist.github.com/clragon/cadf818800c86b60b9c560673ecf6356
this adds a guard function matching each ProxyProvider / ChangeNotifierProxyProvider variant and works similar to a select by checking a list of values for deep equality. It is used like this:

class _MyPageState extends State<MyPage> {
  @override
  Widget build(BuildContext context) => ProxyProvider<Settings, Client>(
    update: guard(
      create: (context, value, value2) => Client(
        settings: value,
        dispose: (context, value) => value.dispose,
      ),
      dispose: (context, value) => value.dispose,
      child: /* whatever */,
    ),
  );
}

there are some problems with this, for example that it requires a mixin to hold the state for comparison and because the creation method is now held inside the guard function, I need to also pass the dispose function twice.

If this functionality was provided inbuilt in some way, it would probably be easier and nicer.
I am not sure how exactly this would take shape in the library, however.

edit: I have just thought about it, and this might be possible if I were to implement my own Provider based on one of the for this made classes. I haven't really looked at them before, so I don't know however.

Describe alternatives you've considered
Many of the alternatives have been listed above, but are undesirable in some way.

Additional context
I have setup a small project to play around with this conundrum.
In there, I am manually writing out all the code necessary to make this work without the guard mixin.

Code
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// Parent class, represents a dependency of the Child
class Parent {
  Object id;

  Parent() : id = Object();
}

// Child class. depends on the parent but also holds own state.
// Becomes invalid if the Parent changes.
class Child with ChangeNotifier {
  final Parent parent;
  Object id;

  Child(this.parent) : id = Object();

  void changeId() {
    id = Object();
    notifyListeners();
  }
}

void main() {
  // intentionally disabled, check further down
  Provider.debugCheckInvalidValueType = null;
  runApp(
    const MyApp(),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // Parent can be replaced in some way.
  Parent parent = Parent();

  // keeps track of the previous parent
  Parent? _previousParent;

  @override
  Widget build(BuildContext context) => Provider.value(
        value: parent,
        // cannot use ChangeNotifierProxyProvider because it would force me
        // to change my Child class to accept a Parent in a method and not the constructor,
        // allowing it to be in an invalid state, where no parent is available.
        // this is kind of okay as long as the user always knows about this, but even then its not particularly nice.
        builder: (context, child) => ProxyProvider<Parent, Child>(
          update: (context, value, previous) {
            // prevents the Child state from being discarded if we are rebuilt without the Parent changing.
            // this requires us to hold the previous parent in state somewhere.
            // it also becomes very verbose the more parents we have, and is even with a single one quite much to write everytime.
            if (value != _previousParent) {
              _previousParent = value;
              return Child(value);
            } else {
              return previous!;
            }
          },
          builder: (context, child) => Scaffold(
            appBar: AppBar(
              title: const Text('Example'),
            ),
            body: Center(
              // listening to child changes because we are not using ChangeNotifierProxyProvider
              child: AnimatedBuilder(
                animation: Provider.of<Child>(context),
                builder: (context, child) {
                  return Column(
                    mainAxisSize: MainAxisSize.min,
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text(
                          'Parent: ${Provider.of<Parent>(context).id.hashCode}'),
                      Text('Child: ${Provider.of<Child>(context).id.hashCode}'),
                      const SizedBox(height: 24),
                      Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: [
                          OutlinedButton(
                            onPressed:
                                Provider.of<Child>(context, listen: false)
                                    .changeId,
                            child: const Text('change child ID'),
                          ),
                          const SizedBox(width: 8),
                          OutlinedButton(
                            onPressed: () => setState(() => parent = Parent()),
                            child: const Text('change parent'),
                          ),
                        ],
                      ),
                    ],
                  );
                },
              ),
            ),
            // would cause the Child state to be discarded if we were not guarding it with an If statement.
            floatingActionButton: FloatingActionButton.extended(
              onPressed: () => setState(() {}),
              label: const Text('random setState'),
              icon: const Icon(Icons.shuffle),
            ),
          ),
        ),
      );
}
@clragon clragon added enhancement New feature or request needs triage labels Jul 10, 2022
@rrousselGit
Copy link
Owner

Hello!

I don't think it makes sense to use create if you're storing the state outside of the provider.
If you want such a thing, consider using MyProvider.value instead

Using Provider.value you should have enough to make your own "proxy"variants which better fit your use-case, without forcing users to use a StatefulWidget+mixin to use them

@clragon
Copy link
Author

clragon commented Jul 10, 2022

Hi,

I am not sure if I can follow what you mean.
The only reason I am storing state is so that I can know when my dependencies have changed,
so that my object that I am providing can be recreated.

Of course I could create my object inside of didChangeDependencies and use Provider.value which would have a similar outcome, meaning it would could be rebuilt arbitrarily and I would have to shield it with several if statements that check against the last known value of those dependencies.
To make such a structure reusable, I would still have to use a mixin in the end, or not?

@rrousselGit
Copy link
Owner

You wouldn't need the mixin, because you could make a widget instead

You can create a reusable StatefulWidget which returns a provider and does the dependency tracking.

@clragon
Copy link
Author

clragon commented Jul 11, 2022

I see, that makes sense to me.
Thank you for your help, I will solve this in that way then!

@clragon clragon closed this as completed Jul 11, 2022
@clragon
Copy link
Author

clragon commented Jul 11, 2022

I have created a StatefulWidget that does the things I require:

SelectiveProvider
typedef SelectiveProviderBuilder0<R> = R Function(BuildContext context);

typedef SelectiveProviderBuilder<T, R> = R Function(
  BuildContext context,
  T value,
);

typedef SelectiveProviderBuilder2<T, T2, R> = R Function(
  BuildContext context,
  T value,
  T2 value2,
);

typedef SelectiveProviderBuilder3<T, T2, T3, R> = R Function(
  BuildContext context,
  T value,
  T2 value2,
  T3 value3,
);

typedef SelectiveProviderBuilder4<T, T2, T3, T4, R> = R Function(
  BuildContext context,
  T value,
  T2 value2,
  T3 value3,
  T4 value4,
);

typedef SelectiveProviderBuilder5<T, T2, T3, T4, T5, R> = R Function(
  BuildContext context,
  T value,
  T2 value2,
  T3 value3,
  T4 value4,
  T5 value5,
);

typedef SelectiveProviderBuilder6<T, T2, T3, T4, T5, T6, R> = R Function(
  BuildContext context,
  T value,
  T2 value2,
  T3 value3,
  T4 value4,
  T5 value5,
  T6 value6,
);

typedef SelectiveBuilder = List<dynamic>? Function(BuildContext context);

class SelectiveProvider0<R> extends SingleChildStatefulWidget {
  final Widget? child;
  final TransitionBuilder? builder;
  final SelectiveProviderBuilder0<R> create;
  final Dispose<R>? dispose;
  final SelectiveProviderBuilder0<List<dynamic>>? selector;
  final bool? notifier;

  const SelectiveProvider0({
    super.key,
    this.child,
    this.builder,
    required this.create,
    this.dispose,
    this.selector,
    this.notifier,
  }) : super(child: child);

  @override
  State<SelectiveProvider0<R>> createState() => _SelectiveProvider0State<R>();
}

class _SelectiveProvider0State<R>
    extends SingleChildState<SelectiveProvider0<R>> {
  List<dynamic>? dependencies;
  R? value;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final List<dynamic>? conditions = widget.selector?.call(context);
    final List<dynamic> values = [if (conditions != null) conditions];
    if (!const DeepCollectionEquality().equals(dependencies, values)) {
      if (value != null) {
        widget.dispose?.call(context, value as R);
      }
      value = widget.create(context);
      dependencies = values;
    }
  }

  @override
  void dispose() {
    widget.dispose?.call(context, value as R);
    super.dispose();
  }

  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    assert(
      widget.builder != null || child != null,
      '$runtimeType used outside of MultiProvider must specify a child',
    );
    return Provider.value(
      value: value as R,
      child: widget.builder != null
          ? Builder(
              builder: (context) => widget.builder!(context, child),
            )
          : child!,
    );
  }
}

class SelectiveProvider<T, R> extends SelectiveProvider0<R> {
  SelectiveProvider({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder<T, R> create,
    super.dispose,
    SelectiveProviderBuilder<T, List<dynamic>>? selector,
  }) : super(
          create: (context) => create(
            context,
            Provider.of<T>(context),
          ),
          selector: (context) =>
              (selector?.call(context, Provider.of<T>(context)) ?? [])
                ..add(Provider.of<T>(context)),
        );
}

class SelectiveProvider2<T, T2, R> extends SelectiveProvider<T, R> {
  SelectiveProvider2({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder2<T, T2, R> create,
    super.dispose,
    SelectiveProviderBuilder2<T, T2, List<dynamic>>? selector,
  }) : super(
          create: (context, value) => create(
            context,
            value,
            Provider.of<T2>(context),
          ),
          selector: (context, value) =>
              (selector?.call(context, value, Provider.of<T2>(context)) ?? [])
                ..add(Provider.of<T2>(context)),
        );
}

class SelectiveProvider3<T, T2, T3, R> extends SelectiveProvider2<T, T2, R> {
  SelectiveProvider3({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder3<T, T2, T3, R> create,
    super.dispose,
    SelectiveProviderBuilder3<T, T2, T3, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2) => create(
            context,
            value,
            value2,
            Provider.of<T3>(context),
          ),
          selector: (context, value, value2) => (selector?.call(
                  context, value, value2, Provider.of<T3>(context)) ??
              [])
            ..add(Provider.of<T3>(context)),
        );
}

class SelectiveProvider4<T, T2, T3, T4, R>
    extends SelectiveProvider3<T, T2, T3, R> {
  SelectiveProvider4({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder4<T, T2, T3, T4, R> create,
    super.dispose,
    SelectiveProviderBuilder4<T, T2, T3, T4, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2, value3) => create(
            context,
            value,
            value2,
            value3,
            Provider.of<T4>(context),
          ),
          selector: (context, value, value2, value3) => (selector?.call(
                  context, value, value2, value3, Provider.of<T4>(context)) ??
              [])
            ..add(Provider.of<T4>(context)),
        );
}

class SelectiveProvider5<T, T2, T3, T4, T5, R>
    extends SelectiveProvider4<T, T2, T3, T4, R> {
  SelectiveProvider5({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder5<T, T2, T3, T4, T5, R> create,
    super.dispose,
    SelectiveProviderBuilder5<T, T2, T3, T4, T5, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2, value3, value4) => create(
            context,
            value,
            value2,
            value3,
            value4,
            Provider.of<T5>(context),
          ),
          selector: (context, value, value2, value3, value4) => (selector?.call(
                  context,
                  value,
                  value2,
                  value3,
                  value4,
                  Provider.of<T5>(context)) ??
              [])
            ..add(Provider.of<T5>(context)),
        );
}

class SelectiveProvider6<T, T2, T3, T4, T5, T6, R>
    extends SelectiveProvider5<T, T2, T3, T4, T5, R> {
  SelectiveProvider6({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder6<T, T2, T3, T4, T5, T6, R> create,
    super.dispose,
    SelectiveProviderBuilder6<T, T2, T3, T4, T5, T6, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2, value3, value4, value5) => create(
            context,
            value,
            value2,
            value3,
            value4,
            value5,
            Provider.of<T6>(context),
          ),
          selector: (context, value, value2, value3, value4, value5) =>
              (selector?.call(context, value, value2, value3, value4, value5,
                      Provider.of<T6>(context)) ??
                  [])
                ..add(Provider.of<T6>(context)),
        );
}

as well as one for ChangeNotifier:

SelectiveChangeNotifierProvider
class SelectiveChangeNotifierProvider0<R extends ChangeNotifier?>
    extends SelectiveProvider0<R> {
  const SelectiveChangeNotifierProvider0({
    super.key,
    super.child,
    super.builder,
    required super.create,
    super.dispose,
    super.selector,
  });

  @override
  State<SelectiveProvider0<R>> createState() =>
      _SelectiveChangeNotifierProvider0<R>();
}

class _SelectiveChangeNotifierProvider0<R extends ChangeNotifier?>
    extends _SelectiveProvider0State<R> {
  @override
  Widget buildWithChild(BuildContext context, Widget? child) {
    assert(
      widget.builder != null || child != null,
      '$runtimeType used outside of MultiProvider must specify a child',
    );
    return ChangeNotifierProvider.value(
      value: value as R,
      child: widget.builder != null
          ? Builder(
              builder: (context) => widget.builder!(context, child),
            )
          : child!,
    );
  }
}

class SelectiveChangeNotifierProvider<T, R extends ChangeNotifier?>
    extends SelectiveChangeNotifierProvider0<R> {
  SelectiveChangeNotifierProvider({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder<T, R> create,
    super.dispose,
    SelectiveProviderBuilder<T, List<dynamic>>? selector,
  }) : super(
          create: (context) => create(
            context,
            Provider.of<T>(context),
          ),
          selector: (context) =>
              (selector?.call(context, Provider.of<T>(context)) ?? [])
                ..add(Provider.of<T>(context)),
        );
}

class SelectiveChangeNotifierProvider2<T, T2, R extends ChangeNotifier?>
    extends SelectiveChangeNotifierProvider<T, R> {
  SelectiveChangeNotifierProvider2({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder2<T, T2, R> create,
    super.dispose,
    SelectiveProviderBuilder2<T, T2, List<dynamic>>? selector,
  }) : super(
          create: (context, value) => create(
            context,
            value,
            Provider.of<T2>(context),
          ),
          selector: (context, value) =>
              (selector?.call(context, value, Provider.of<T2>(context)) ?? [])
                ..add(Provider.of<T2>(context)),
        );
}

class SelectiveChangeNotifierProvider3<T, T2, T3, R extends ChangeNotifier?>
    extends SelectiveChangeNotifierProvider2<T, T2, R> {
  SelectiveChangeNotifierProvider3({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder3<T, T2, T3, R> create,
    super.dispose,
    SelectiveProviderBuilder3<T, T2, T3, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2) => create(
            context,
            value,
            value2,
            Provider.of<T3>(context),
          ),
          selector: (context, value, value2) => (selector?.call(
                  context, value, value2, Provider.of<T3>(context)) ??
              [])
            ..add(Provider.of<T3>(context)),
        );
}

class SelectiveChangeNotifierProvider4<T, T2, T3, T4, R extends ChangeNotifier?>
    extends SelectiveChangeNotifierProvider3<T, T2, T3, R> {
  SelectiveChangeNotifierProvider4({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder4<T, T2, T3, T4, R> create,
    super.dispose,
    SelectiveProviderBuilder4<T, T2, T3, T4, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2, value3) => create(
            context,
            value,
            value2,
            value3,
            Provider.of<T4>(context),
          ),
          selector: (context, value, value2, value3) => (selector?.call(
                  context, value, value2, value3, Provider.of<T4>(context)) ??
              [])
            ..add(Provider.of<T4>(context)),
        );
}

class SelectiveChangeNotifierProvider5<T, T2, T3, T4, T5,
        R extends ChangeNotifier?>
    extends SelectiveChangeNotifierProvider4<T, T2, T3, T4, R> {
  SelectiveChangeNotifierProvider5({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder5<T, T2, T3, T4, T5, R> create,
    super.dispose,
    SelectiveProviderBuilder5<T, T2, T3, T4, T5, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2, value3, value4) => create(
            context,
            value,
            value2,
            value3,
            value4,
            Provider.of<T5>(context),
          ),
          selector: (context, value, value2, value3, value4) => (selector?.call(
                  context,
                  value,
                  value2,
                  value3,
                  value4,
                  Provider.of<T5>(context)) ??
              [])
            ..add(Provider.of<T5>(context)),
        );
}

class SelectiveChangeNotifierProvider6<T, T2, T3, T4, T5, T6,
        R extends ChangeNotifier?>
    extends SelectiveChangeNotifierProvider5<T, T2, T3, T4, T5, R> {
  SelectiveChangeNotifierProvider6({
    super.key,
    super.child,
    super.builder,
    required SelectiveProviderBuilder6<T, T2, T3, T4, T5, T6, R> create,
    super.dispose,
    SelectiveProviderBuilder6<T, T2, T3, T4, T5, T6, List<dynamic>>? selector,
  }) : super(
          create: (context, value, value2, value3, value4, value5) => create(
            context,
            value,
            value2,
            value3,
            value4,
            value5,
            Provider.of<T6>(context),
          ),
          selector: (context, value, value2, value3, value4, value5) =>
              (selector?.call(context, value, value2, value3, value4, value5,
                      Provider.of<T6>(context)) ??
                  [])
                ..add(Provider.of<T6>(context)),
        );
}

these are very verbose to write, however, easy to use:

class RemoteDataProvider extends SelectiveChangeNotifierProvider2<Settings, Client, RemoteDataService> {
  RemoteDataProvider({String? url})
      : super(
          create: (context, settings, client) => RemoteDataService(
            url: url,
            settings: settings,
            client: client,
          ),
        );
}

and can be adapted for sub-properties:

class DatabaseProvider extends SelectiveChangeNotifierProvider<Settings, DatabaseService> {
  DatabaseProvider()
      : super(
          create: (context, settings) => DatabaseService(
            path: settings.dabatasePath,
          ),
          selector: (context, settings) => [settings.dabatasePath],
        );
}

There might be a way to write this a bit shorter and conciser.
If you happen to know of such a way I would be very interested.

I also first thought I might be able to build this by extending InheritedProvider, but none of the _Delegate structures are public. if they were, I think I mightve been able to use their State to keep track of the selections.

Maybe a similar structure to the above could be built inside of the library with a _Delegate.
This could perhaps help alleviate the whole "depending on another Provider in a Provider is difficult".

Do you think this would be worthwile, or this usecase just very specific to me?

@clragon clragon reopened this Jul 11, 2022
@clragon
Copy link
Author

clragon commented Jul 15, 2022

I have given this some though and I believe the most elegant way of integrating this would be to add a "selector" parameter to ProxyProvider and ChangeNotifierProxyProvider (and maybe normal Provider too), which accepts a List<dynamic> Function(BuildContext). Perhaps the List<dynamic> could also be a generic type instead, so that type safety can be retained partially.
The list would then be compared with DeepCollectionEquality and if it changed, the create function would be called again.

If that list by default would be empty, this could be done with full backwards compatibility, no breaking changes.
If the list by default contains all other Providers we depend on, that would perhaps be a breaking change, but very convenient for my usecase.

This would also enable "stateless" (without a statefulwidget) recreation of Providers that depend on a Parameter that is passed in, for example an ID or something and for which it would be easier to recreate them than to reuse them like the update function in ChangeNotifierProxyProvider does.

@clragon
Copy link
Author

clragon commented Jul 15, 2022

i think this might be too specific to my usecase to be part of the library.
However, it would be alot simpler to implement if we had access to the delegate classes of the inbuilt providers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request needs triage
Projects
None yet
Development

No branches or pull requests

2 participants