rrousselGit / riverpod

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

autoDispose is not working when the widget is disposed,and the same widget is registered again after the previous widget was disposed #3496

Closed Trung15010802 closed 3 weeks ago

Trung15010802 commented 3 weeks ago

Describe the bug I have two screen contain the same widget and i register a listener by using ref.watch on that widget. But when move from screen1 to screen2. Widget is disposed but provider don't.

To Reproduce

import 'dart:math';

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

void main() => runApp(const ProviderScope(child: TabBarApp()));

final countProvider = StateProvider.autoDispose<int>((ref) {
  debugPrint('countProvider initialized');
  ref.onDispose(() {
    debugPrint('countProvider disposed');
  });

  return 0;
});

class TabBarApp extends StatelessWidget {
  const TabBarApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: const TabBarExample(),
    );
  }
}

class TabBarExample extends StatelessWidget {
  const TabBarExample({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      initialIndex: 0,
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('TabBar Sample'),
          bottom: const TabBar(
            tabs: <Widget>[
              Tab(
                icon: Icon(Icons.cloud_outlined),
              ),
              Tab(
                icon: Icon(Icons.beach_access_sharp),
              ),
            ],
          ),
        ),
        body: const TabBarView(
          children: <Widget>[
            Tile(),
            Tile(),
          ],
        ),
        floatingActionButton: Consumer(builder: (context, ref, child) {
          return FloatingActionButton(
            onPressed: () {
              ref.read(countProvider.notifier).update((state) => ++state);
            },
            child: const Icon(Icons.add),
          );
        }),
      ),
    );
  }
}

class Tile extends ConsumerStatefulWidget {
  const Tile({super.key});

  @override
  ConsumerState<Tile> createState() => _TileState();
}

class _TileState extends ConsumerState<Tile> {
  late final Color color;

  @override
  void initState() {
    color = randomColor();
    super.initState();
  }

  @override
  void dispose() {
    debugPrint('Tile widget disposed');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    final count = ref.watch(countProvider);
    return Container(
      color: color,
      alignment: Alignment.center,
      child: Text(
        count.toString(),
        style: const TextStyle(
          fontSize: 24,
          color: Colors.white,
          shadows: [
            Shadow(color: Colors.black, offset: Offset(1, 1), blurRadius: 1)
          ],
        ),
      ),
    );
  }
}

Color randomColor() {
  final random = Random();
  return Color.fromARGB(
    255,
    random.nextInt(256),
    random.nextInt(256),
    random.nextInt(256),
  );
}

Expected behavior I think the provider should be dispose after previous widget is disposed and create a new provider in the next widget

charlescyt commented 3 weeks ago

It's just how TabBarView works. The new tile is built before the old tile is removed and thus the provider is being watched the whole time.

Trung15010802 commented 3 weeks ago

It's just how TabBarView works. The new tile is built before the old tile is removed and thus the provider is being watched the whole time.


import 'dart:math';

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

void main() => runApp(const ProviderScope(child: TabBarApp()));

final countProvider = StateProvider.autoDispose((ref) { debugPrint('countProvider initialized'); ref.onDispose(() { debugPrint('countProvider disposed'); });

return 0; });

class TabBarApp extends StatelessWidget { const TabBarApp({super.key});

@override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData(useMaterial3: true), home: const TabBarExample(), ); } }

List tabs = [ Tile( key: UniqueKey(), ), Tile( key: UniqueKey(), ) ];

class TabBarExample extends StatefulWidget { const TabBarExample({super.key});

@override State createState() => _TabBarExampleState(); }

class _TabBarExampleState extends State { int _currentIndex = 0; @override Widget build(BuildContext context) { return DefaultTabController( initialIndex: 0, length: 2, child: Scaffold( appBar: AppBar( title: const Text('TabBar Sample'), bottom: TabBar( tabs: const [ Tab( icon: Icon(Icons.cloud_outlined), ), Tab( icon: Icon(Icons.beach_access_sharp), ), ], onTap: (value) { setState(() { _currentIndex = value; }); }, ), ), body: tabs[_currentIndex], floatingActionButton: Consumer(builder: (context, ref, child) { return FloatingActionButton( onPressed: () { ref.read(countProvider.notifier).update((state) => ++state); }, child: const Icon(Icons.add), ); }), ), ); } }

class Tile extends ConsumerStatefulWidget { const Tile({super.key});

@override ConsumerState createState() => _TileState(); }

class _TileState extends ConsumerState { late final Color color;

@override void initState() { color = randomColor(); super.initState(); }

@override void dispose() { debugPrint('Tile widget disposed'); super.dispose(); }

@override Widget build(BuildContext context) { final count = ref.watch(countProvider); return Container( color: color, alignment: Alignment.center, child: Text( count.toString(), style: const TextStyle( fontSize: 24, color: Colors.white, shadows: [ Shadow(color: Colors.black, offset: Offset(1, 1), blurRadius: 1) ], ), ), ); } }

Color randomColor() { final random = Random(); return Color.fromARGB( 255, random.nextInt(256), random.nextInt(256), random.nextInt(256), ); }



I don't use TabBarView anymore but still not work as my expectation
charlescyt commented 3 weeks ago

Try adding a print statement inside the build method and you will notice that the disposal of the old widget happens after the creation of the new widget.

Trung15010802 commented 3 weeks ago

Try adding a print statement inside the build method and you will notice that the disposal of the old widget happens after the creation of the new widget.

So could you show me a way to achieve my my goal, pls?

charlescyt commented 3 weeks ago

What's your use case? A simple workaround would be calling ref.invalidate when changing tabs.

Trung15010802 commented 3 weeks ago

What's your use case? A simple workaround would be calling ref.invalidate when changing tabs.

So autoDispose have no effect in my case? And i have to dispose it manualy. I can't use ref.invalidate in dispose() method

AhmedLSayed9 commented 3 weeks ago

That's expected.

The provider is there in the widget tree when you switch between tabs. To be disposed, it has to not exist in the widget tree for at least 1 frame, which doesn't happen in your case.

You can check ref.onCancel and ref.onResume to ensure it has correctly removed the last listener and then the provider is listened to again. Or, you can just check ref.onAddListener and ref.onRemoveListener.

If you need the provider to be specific to a widget, you can use family provider and pass some cached key related to the widget. In that case, both widgets will be using a different version of the same provider.

Trung15010802 commented 3 weeks ago

That's expected.

The provider is there in the widget tree when you switch between tabs. To be disposed, it has to not exist in the widget tree for at least 1 frame, which doesn't happen in your case.

You can check ref.onCancel and ref.onResume to ensure it has correctly removed the last listener and then the provider is listened to again. Or, you can just check ref.onAddListener and ref.onRemoveListener.

If you need the provider to be specific to a widget, you can use family provider and pass some cached key related to the widget. In that case, both widgets will be using a different version of the same provider.

Thank you very much. Now i understand