riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

`ref.exists()` returns `true` when manually invalidate a provider.

Open hasanmhallak opened this issue 1 month ago • 0 comments

Describe the bug It appears that ref.exist(someProvider) still returns true when manually invalidate a provider, and when try to use the invalidated provider onResume gets called. While when we set keepAlive to true or use AutoDisposeNotifier everything works as expected.

This behavior is found when using / not using generated code.

Original issue #2729

To Reproduce

Code Generation Sample

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'main.g.dart';

@Riverpod(keepAlive: true)
int fetchItem(FetchItemRef ref) {
  // this will be called on invalidate.
  ref.onDispose(() => print('dispose'));

  ref.onCancel(() => print('onCancel'));

  // but this will be called when we use
  // the provider after invalidating
  ref.onResume(() => print('onResume'));
  return 5;
}

void main() {
  runApp(
    const ProviderScope(child: MaterialApp(home: MyHomePage())),
  );
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(onPressed: () {
        print('does exist: ${ref.exists(fetchItemProvider)}');
      }),
      body: Center(
        child: TextButton(
          onPressed: () {
            print('Navigate To Next Page');
            Navigator.of(context).push(MaterialPageRoute(builder: (_) => MyHomePage2()));
          },
          child: Text('Navigate To Next Page'),
        ),
      ),
    );
  }
}

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

  @override
  ConsumerState<MyHomePage2> createState() => _MyHomePage2State();
}

class _MyHomePage2State extends ConsumerState<MyHomePage2> {
  ProviderContainer? _container;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _container!.invalidate(fetchItemProvider);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _container = ProviderScope.containerOf(context);
    return Scaffold(
      appBar: AppBar(),
      floatingActionButton: FloatingActionButton(onPressed: () {
        print('does exist: ${ref.exists(fetchItemProvider)}');
      }),
      body: Center(
        child: Text(ref.watch(fetchItemProvider).toString()),
      ),
    );
  }
}
No Code Generation Sample with explanation

To Reproduce

  1. run the code
  2. Click Exist Fab
  3. Navigate to next page
  4. Click the + Fab to increment state
  5. Click Exist
  6. Navigate Back
  7. Click Exist

Logs

flutter: does exist: false
flutter: internal State: 5
flutter: Navigate To Next Page
flutter: does exist: true
flutter: internal State: 7
flutter: dispose
flutter: does exist: true // the instance still exists and the internal state is preserved.
flutter: internal State: 7

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

class FetchItemsNotifier extends Notifier<int> {
  int internalState = 5;
  @override
  build() {
    // this will be called on invalidate.
    ref.onDispose(() => print('dispose'));

    ref.onCancel(() => print('onCancel'));

    // but this will be called when we use
    // the provider after invalidating
    ref.onResume(() => print('onResume'));
    return 5;
  }

  void add() {
    state = state + 1;
    internalState = state;
  }
}

final fetchItemProvider = NotifierProvider<FetchItemsNotifier, int>(FetchItemsNotifier.new);

void main() {
  runApp(
    const ProviderScope(child: MaterialApp(home: MyHomePage())),
  );
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        child: Text('Exist'),
        onPressed: () {
          print('does exist: ${ref.exists(fetchItemProvider)}');
          print('internal State: ${ref.read(fetchItemProvider.notifier).internalState}');
        },
      ),
      body: Center(
        child: TextButton(
          onPressed: () {
            print('Navigate To Next Page');
            Navigator.of(context).push(MaterialPageRoute(builder: (_) => MyHomePage2()));
          },
          child: Text('Navigate To Next Page'),
        ),
      ),
    );
  }
}

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

  @override
  ConsumerState<MyHomePage2> createState() => _MyHomePage2State();
}

class _MyHomePage2State extends ConsumerState<MyHomePage2> {
  ProviderContainer? _container;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    _container!.invalidate(fetchItemProvider);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _container = ProviderScope.containerOf(context);
    return Scaffold(
      appBar: AppBar(),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'Exist',
            child: Text('Exist'),
            onPressed: () {
              print('does exist: ${ref.exists(fetchItemProvider)}');
              print('internal State: ${ref.read(fetchItemProvider.notifier).internalState}');
            },
          ),
          FloatingActionButton(
            child: Text('+'),
            onPressed: () {
              ref.read(fetchItemProvider.notifier).add();
            },
          ),
        ],
      ),
      body: Center(
        child: Text(ref.watch(fetchItemProvider).toString()),
      ),
    );
  }
}

When we switch to using AutoDisposeNotifier the internal state is resets (which means the old provider instance are removed from the container) and ref.exists() return false. which will produce the following logs:

**Logs When Using AutoDisposeNotifier **

flutter: does exist: false
flutter: internal State: 5
flutter: dispose // got disposed after accessing the internal state of the provider when no active listener registered.
flutter: Navigate To Next Page
flutter: does exist: true
flutter: internal State: 8
flutter: dispose 
flutter: does exist: false // auto dispose works as expected.
flutter: internal State: 5 // a new AutoDisposeNotifier instance created
flutter: dispose // got disposed after accessing the internal state of the provider when no active listener registered.

hasanmhallak avatar May 20 '24 07:05 hasanmhallak