rrousselGit / riverpod

A reactive caching and data-binding framework. Riverpod makes working with asynchronous code a breeze.
https://riverpod.dev
MIT License
6.28k stars 955 forks source link

How does autoDispose work? #400

Closed darwin-morocho closed 3 years ago

darwin-morocho commented 3 years ago

Hi @rrousselGit thanks for this proyecto. Could you help me to understand something?

I have a project with a personal state managment based of Provider now I am changing the logic to be similar to riverpod but I was not able to replicate the autoDispose feature.

How a provider can know that it no longer needs it if the Provider is not linked to a statefulwidget?

Thanks for your response

venkatd commented 3 years ago

@darwin-morocho an autoDispose provider considers a provider is no longer being when it is not being used in any widgets or any dependent providers. That's my understanding of it.

darwin-morocho commented 3 years ago

@darwin-morocho an autoDispose provider considers a provider is no longer being when it is not being used in any widgets or any dependent providers. That's my understanding of it.

Hi @venkatd you are right but consider the next code


class CounterController extends StateNotifier<int> {
  CounterController() : super(0);
  void increment() => state++;
}

final counterProvider = StateNotifierProvider.autoDispose(
  (ref) {
    ref.onDispose(() {
      print("onDispose counter");
    });
    print("new Counter");
    return CounterController();
  },
);

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

  @override
  _DemoState createState() => _DemoState();
}

class _DemoState extends State<Demo> {
  @override
  void initState() {
    super.initState();
    context.read(counterProvider).addListener((state) {
      print("Demo State addListener");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, watch) {
    return Scaffold(
      appBar: AppBar(
        leading: Demo(),
        actions: [
          IconButton(
            icon: Icon(Icons.login),
            onPressed: () {
              final route = MaterialPageRoute(
                builder: (_) => LoginPage(),
              );
              Navigator.pushReplacement(context, route);
            },
          ),
        ],
      ),
      body: Center(
        child: Text("counter"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read(counterProvider).increment();
        },
      ),
    );
  }
}

The CounterPage doesn't use the watch method to listen the counterProvider but the _stateDemo has a listener to listen the changes in my CounterController if I use context.read(counterProvider).increment(); to increment the counter the counterPrivoder creates a new instance of CounterControllerit is destroyed immediately and the listener in _stateDemo is never called.

But if I use the watch method inside the build method of my CounterPage the CounterController is destroyed when the counter page is destroyed.

davidmartos96 commented 3 years ago

@darwin-morocho You could replace your addListener with a ProviderListener widget inside the build method of _DemoState.

venkatd commented 3 years ago

That's a good observation. I should note that, watch is what you would be recommended to use. It tells the system "I need this value here". While read is like saying "give me whatever is here right now".

You can't rely on read to make sure something will stay available for you.

rrousselGit commented 3 years ago

autoDispose relies on how many listeners a provider has, not how many listeners the exposed object has

StateNotifier.addListener has no impact.

darwin-morocho commented 3 years ago

@darwin-morocho You could replace your addListener with a ProviderListener widget inside the build method of _DemoState.


class _DemoState extends State<Demo> {
  @override
  void initState() {
    super.initState();
    context.read(counterProvider).addListener((state) {
      print("Demo State addListener");
    });
  }

  @override
  Widget build(BuildContext context) {
    return ProviderListener(
      onChange: (_, __) {},
      provider: counterProvider,
      child: Container(),
    );
  }
}

Using ProviderListener the ConterController is created and and destroyed immediately

flutter: new Counter
flutter: Demo State addListener
flutter: onDispose counter
davidmartos96 commented 3 years ago

@darwin-morocho I think that dispose comes from the read inside the initState, but can't tell for sure. With the ProviderListener you don't need the initState code. You can put the print inside the onChange callback.

venkatd commented 3 years ago

@darwin-morocho I would put a print statement inside of initState and inside of build. You're probably calling context.read before build is ever called which triggers the quick create/destroy.

I would also recommend taking a look at hooks_riverpod which, IMO, simplifies dealing with widget lifecycles and accessing provider values.

rrousselGit commented 3 years ago

@darwin-morocho Your snippet is wrong I tried this myself and the provider is not disposed.

There's most likely something other than what you gave that causes dispose to be called.

venkatd commented 3 years ago

@rrousselGit related to this, how long does riverpod wait before disposing an unused provider? Is it a single frame?

rrousselGit commented 3 years ago

Yes, a frame

As such, the read inside initState followed ProviderListener cannot cause the provider to be disposed between the read and the creation of ProviderListener. Because both reads are within the same frame.

davidmartos96 commented 3 years ago

Yes, a frame

As such, the read inside initState followed ProviderListener cannot cause the provider to be disposed between the read and the creation of ProviderListener. Because both reads are within the same frame.

Useful to know!

darwin-morocho commented 3 years ago

@darwin-morocho Your snippet is wrong I tried this myself and the provider is not disposed.

There's most likely something other than what you gave that causes dispose to be called.

This is the complete code


void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: CounterPage(),
    );
  }
}

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class CounterController extends StateNotifier<int> {
  CounterController() : super(0);
  void increment() => state++;
}

final counterProvider = StateNotifierProvider.autoDispose(
  (ref) {
    ref.onDispose(() {
      print("onDispose counter");
    });
    print("new Counter");
    return CounterController();
  },
);

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

  @override
  _DemoState createState() => _DemoState();
}

class _DemoState extends State<Demo> {
  @override
  void initState() {
    super.initState();
    context.read(counterProvider).addListener((state) {
      print("Demo State addListener");
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, watch) {
    return Scaffold(
      appBar: AppBar(
        title: Demo(),
      ),
      body: Center(
        child: Text("counter"),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context.read(counterProvider).increment();
        },
      ),
    );
  }
}

If Do I not use the watch method or even a ProviderListener my provider should not be destroyed?

https://user-images.githubusercontent.com/15864336/113493604-59fa7400-94a6-11eb-97a1-b252d42f60c9.mov

rrousselGit commented 3 years ago

If Do I not use the watch method or even a ProviderListener my provider should not be destroyed?

That's the opposite.

Because you are not using watch/ProviderListener, then your provider is destroyed.

Use ProviderListener instead of initState + read(provider).addListener

darwin-morocho commented 3 years ago

If Do I not use the watch method or even a ProviderListener my provider should not be destroyed?

That's the opposite.

Because you are not using watch/ProviderListener, then your provider is destroyed.

Use ProviderListener instead of initState + read(provider).addListener

Thanks Now I understand How the auto dispose works