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

Invalidating an AutoDisposeProvider A that watches AutoDisposeProvider B doesn't dispose AutoDisposeProvider B automatically. #3455

Open aldee opened 1 month ago

aldee commented 1 month ago

Describe the bug Let's say I have a Widget that depends on FutureProvider A, and then FutureProvider A depends on FutureProvider B. The Widget doesn't know about FutureProvider B. If FutureProvider B returns an error, FutureProvider A will return an error as well to the Widget. When the Widget got an Error, it would show an option to refresh by calling WidgetRef.invalidate() on FutureProvider A.

I assumed when FutureProvider A got invalidated, it would get disposed, then since only FutureProvider A depends on/watches FutureProvider B, FutureProvider B would get disposed of as well, resulting in a recomputed value for FutureProvider B.

But in Riverpod 2.5.0 that I'm using in my project, it still return the previous error/value.

To Reproduce

This is the code that I use to reproduce it:

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

const wholeSentence = 'a sentence + $number';
const number = 100;

void main() {
  test('testing future provider', () async {
    final container = createContainer();

    // First throw an exception
    container.updateOverrides([listenedProvider.overrideWith((ref) => throw Exception())]);
    await expectLater(
      container.read(listeningProvider.future),
      throwsA(isA<Exception>()),
    );

    // Then expect to get new value after invalidate
    container.updateOverrides([listenedProvider.overrideWith((ref) async => number)]);
    container.invalidate(listeningProvider);
    await expectLater(
      container.read(listeningProvider.future),
      completion(wholeSentence),
    );
  });

  testWidgets('testing widget with future provider', (widgetTester) async {
    await widgetTester.pumpWidget(
      ProviderScope(
        overrides: [
          listenedProvider.overrideWith((ref) {
            print('throwing Exception');
            throw Exception();
          }),
        ],
        child: const SomeWidget(),
      ),
    );

    print('setting up container');
    final element = widgetTester.element(find.byType(SomeWidget));
    final container = ProviderScope.containerOf(element);

    print('running first test');
    await expectLater(
      container.read(listeningProvider.future),
      throwsA(isA<Exception>()),
    );

    print('invalidate then get new value from listenedProvider');
    // container.invalidate(listenedProvider);
    container.invalidate(listeningProvider);
    container.updateOverrides([listenedProvider.overrideWith((ref) async => number)]);
    await widgetTester.pumpAndSettle();
    await expectLater(
      container.read(listeningProvider.future),
      completion(wholeSentence),
    );
  });
}

class SomeWidget extends ConsumerWidget {
  const SomeWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final result = ref.watch(listeningProvider);
    if (result.hasValue) {
      return MaterialApp(home: Text(result.value!));
    }
    return Container();
  }
}

final listenedProvider = FutureProvider.autoDispose<int>((ref) async {
  throw UnimplementedError();
});

final listeningProvider = FutureProvider.autoDispose<String>((ref) async {
  final number = await ref.watch(listenedProvider.future);
  return 'a sentence + $number';
});

ProviderContainer createContainer() {
  final container = ProviderContainer(
    overrides: [
      listenedProvider.overrideWith((ref) async {
        return number;
      }),
    ],
  );
  addTearDown(() => container.dispose());

  return container;
}

Expected behavior I expected both the unit test and widget test above should pass successfully. But in reality, it still throws an error. Only when I added ref.invalidate(listenedProvider) would it pass. But I don't want that, since it shouldn't know about listenedProvider/FutureProvider B

aldee commented 1 month ago

pinging @rrousselGit in case you missed this one. is this an intended behaviour? if yes, what's the appropriate approach for that goal?

nateshmbhat commented 2 weeks ago

facing same issue. what's the recommended way to handle this ?