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

FutureProvider disposed and re run first time #559

Closed
EdwynZN opened this issue Jul 1, 2021 · 6 comments
Closed

FutureProvider disposed and re run first time #559

EdwynZN opened this issue Jul 1, 2021 · 6 comments
Labels
documentation Improvements or additions to documentation

Comments

@EdwynZN
Copy link

EdwynZN commented Jul 1, 2021

Following the case of #243 I am experiencing a similar issue but I'm not sure is the same edge case. I have 3 providers

  • StreamProvider.autodispose.family to watch from a DB a particular object
  • StreamProvider.autodispose.family to filter from the previous provider a parameter of interest when is distinct from the previous
  • FutureProvider.autodispose.family to fire an http call when the previous provider returns a different character

To avoid unnecessary boilerplate this is a minimal reproducible example without a local or web repositories

/// A testable model
class Profile {
  final int uuid;
  final String character;

  Profile(this.uuid, this.character);
}

/// simulating an object returned from the repository
final _profileProvider = StreamProvider.autoDispose.family<Profile?, int>(
  (ref, key) async* {
    final streamController = StreamController<int>();

    ref.onDispose(() {
      print('Stream disposed');
    });

    yield key.isEven ? Profile(key, 'even') : Profile(key, 'odd');
    yield* streamController.stream.map((event) =>
        event.isEven ? Profile(event, 'even') : Profile(event, 'odd'));
   /// also tested with return streamController.stream.map((event) => event.isEven ? Profile(event, 'even') : Profile(event, 'odd'));
   ///  with the repository a simple `return box.watch();`
  },
  name: 'Profile Provider',
);

/// the previous provider filtered for changes when character is different
final _characterProvider = StreamProvider.autoDispose.family<String?, int>(
  (ref, key) => ref
      .watch(_profileProvider(key).stream)
      .map<String?>((cb) => cb?.character)
      .distinct(),
  name: 'Character Provider',
);

/// simulating an http call 
final gameProvider =
    FutureProvider.autoDispose.family<List<String>, int>((ref, key) async {
  final characterAsync = ref.watch(_characterProvider(key).last);
  final character = await characterAsync;
  if (character == null) return const [];

  ref.onDispose(() {
    print('disposed');
  });
  late final List<String> result;
  await Future.delayed(const Duration(seconds: 2), () {
    result = List.filled(10, character);
  });

  ref.maintainState = true;
  return result;
}, name: 'Games Provider');


//// Widget
class Example extends StatelessWidget {
  const AppBarExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
          appBar: AppBar(
            title: AppBarExample(),
          ),
          body: Consumer(
            builder: (context, watch, child) {
              return watch(gameProvider(1)).when(
                data: (value) {
                  if (value.isEmpty) return const SizedBox();
                  return ListView.builder(
                    itemBuilder: (context, index) {
                      return ListTile(
                        title: Text(index.toString()),
                      );
                    },
                    itemCount: value.length,
                  );
                },
                loading: () => Center(child: CircularProgressIndicator()),
                error: (_, __) => Center(child: Text('error')),
              );
            },
          ),
        );
  }
}


class AppBarExample extends ConsumerWidget {
  const AppBarExample({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    return watch(_profileProvider(1)).maybeWhen(
      data: (value) {
        if (value != null) return Text('${value.character} : ${value.uuid}');
        return const SizedBox();
      },
      orElse: () => const SizedBox(),
    );
  }
}

From this project gameProvider is disposed and fired again, (I/flutter ( 4643): disposed), _profileProvider and _characterProvider don't get disposed

@EdwynZN EdwynZN added bug Something isn't working needs triage labels Jul 1, 2021
@TimWhiting
Copy link
Collaborator

gameProvider gets disposed when _characterProvider changes, in order to recreate itself based off of the new state of _characterProvider, but because you have maintainState set to true, it doesn't actually ever fully dispose, so _characterProvider has to stay alive because it is depending on it, as well as _profileProvider since _characterProvider depends on that.

final gameProvider =
    FutureProvider.autoDispose.family<List<String>, int>((ref, key) async {
  final characterAsync = ref.watch(_characterProvider(key).last);
  final character = await characterAsync;
  if (character == null) return const [];

  ref.onDispose(() {
    print('disposed');
  });
  late final List<String> result;
  await Future.delayed(const Duration(seconds: 2), () {
    result = List.filled(10, character);
  });
  /// Add this print statement
  print('New game provider built');
  ref.maintainState = true;
  return result;
}, name: 'Games Provider');

By adding the print statement you will see the problem.
You will probably be interested in #779 which will allow you to turn off the maintain state after no one is listening to the provider for a certain amount of time. If that would solve your problem, go ahead and close this issue and subscribe to that one.

@TimWhiting TimWhiting added documentation Improvements or additions to documentation and removed bug Something isn't working needs triage labels Oct 2, 2021
@EdwynZN
Copy link
Author

EdwynZN commented Oct 2, 2021

I understand a new game provider is built and that's why it gets disposed in the first place, but why should it build a new provider though? In this example I'm not reading _characterProvider per se, but the property last :

This can be useful for scenarios where we want to read the current value exposed by a StreamProvider, but also handle the scenario were no value were emitted yet:

And there is only one value in _characterProvider (because of distinct) so _characterProvider has 2 states (AsyncLoading when first created and AsyncValue) but only fired once with the initial value of _profileProvider

Also to add a bit of context of what I'm trying to accomplish (which works, just this weird bug once) is that the user tap a widget an opens a new route with that key (the name of the character) and then _character and _gameProvider starts and fetch the list of games, if loads successfully then maintain it as cache so the next time the user taps the same character is already loaded (almost like the Marvel app example). It works but fires twices disposing the first time because of this behavior.

@TimWhiting
Copy link
Collaborator

Thanks for the clarification, yes this is different. It is more similar to #495. .last is an interesting feature, and I'm not sure if this is how it is intended to work, because last creates a new Future anytime the _characterProvider changes. So it has to rebuild gameProvider so that you get a new characterAsync. So the function gets rebuilt.

It seems like you are trying to keep the state of the previous result while emitting a new result anytime _characterProvider changes. Essentially you want a Stream not a Future:

/// simulating an http call
final gameProvider =
    StreamProvider.autoDispose.family<List<String>, int>((ref, key) async* {
  await for (final character in ref.watch(_characterProvider(key).stream)) {
    if (character == null) {
      yield const [];
    } else {
      ref.onDispose(() {
        print('disposed');
      });
      late final List<String> result;
      await Future.delayed(const Duration(seconds: 2), () {
        result = List.filled(10, character);
      });

      yield result;
    }
  }
}, name: 'Games Provider');

@EdwynZN
Copy link
Author

EdwynZN commented Oct 2, 2021

Thank you for your time, your example is really nice and I will use it for some other things (:wink:) but in this particular example I don't mind the previous state (diffrence with #495 is I'm filtering the profile within the streamProvider distinct method), if the _characterProvider actually returns a different character then so be it gameProvider should rebuild and start an AsyncLoading all over again.

Right now _characterProvider gets the character name from the Profile class and then call distinct to skip data events similar to the previous one (if the name is the same in this case) which means in this example the stream should have only one event, no reason to rebuild gameProvider but still does.

@TimWhiting
Copy link
Collaborator

Ahh, sorry for talking about something else again. However, I believe this issue was fixed in the 1.0.0-dev.0 version of riverpod. At least I'm no longer seeing the 2nd rebuild after migrating this code.

Specifically in the CHANGELOG I see this:

StreamProvider.last, StreamProvider.stream and FutureProvider.future now expose a future/stream that is independent from how many times the associated provider "rebuilt":

  • if a StreamProvider rebuild before its stream emitted any value, StreamProvider.last will resolve with the first value of the new stream instead.
  • if a FutureProvider rebuild before its future completes, FutureProvider.future will resolve with the result of the new future instead.

@EdwynZN
Copy link
Author

EdwynZN commented Oct 3, 2021

I haven't tested it using the last dev 1.0.0 but the CHANGELOg you mention and saying that the 2nd rebuild doesn't appear after migration means that problem was fixed and it was some minor bug so if there is nothing else I will close this issue

@EdwynZN EdwynZN closed this as completed Oct 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

3 participants