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

Unexpected Behavior with ref.exists() in Combination with keepAlive and Provider Dependencies #3134

Closed
ykaito21 opened this issue Nov 20, 2023 · 1 comment
Assignees
Labels
bug Something isn't working needs triage

Comments

@ykaito21
Copy link

Describe the bug
I've encountered unexpected behavior when using ref.exists() in a provider with dependencies, specifically when combined with the keepAlive property in Riverpod. The issue arises when checking the existence of a keepAlive provider within another provider that has dependencies. The expected behavior is for ref.exists() to return true consistently for a keepAlive: true provider, but this doesn't seem to be the case when overridden dependencies with ProviderScope are involved.

To Reproduce

Code sample
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'main.g.dart';

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: _router.routerDelegate,
      routeInformationParser: _router.routeInformationParser,
      routeInformationProvider: _router.routeInformationProvider,
      title: 'Riverpod & GoRouter Demo',
    );
  }
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) {
        return const CounterListScreen();
      },
      routes: [
        GoRoute(
          path: 'counter/:id',
          builder: (context, state) {
            final id = state.pathParameters['id'] ?? '';
            final counter = state.extra as Counter?;
            return ProviderScope(
              overrides: [
                counterProvider.overrideWithValue(counter),
              ],
              child: CounterDetailScreen(counterId: id),
            );
          },
        ),
      ],
    ),
  ],
);

class CounterListScreen extends StatelessWidget {
  const CounterListScreen({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter List')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => context.go('/counter/1', extra: Counter('1')),
          child: const Text('Go to Detail'),
        ),
      ),
    );
  }
}

class CounterDetailScreen extends ConsumerWidget {
  final String counterId;

  const CounterDetailScreen({Key? key, required this.counterId})
      : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final counter = ref.watch(detailScreenCounterProvider(counterId));

    return Scaffold(
      appBar: AppBar(title: const Text('Counter Detail')),
      body: Center(
        child: counter.when(
          data: (data) => Text('Counter ID: ${data.id}'),
          loading: () => const CircularProgressIndicator(),
          error: (e, _) => Text('Error: $e'),
        ),
      ),
    );
  }
}

@Riverpod(keepAlive: false)
Counter? counter(CounterRef ref) {
  throw UnimplementedError();
}

@Riverpod(keepAlive: true)
Future<Counter> fetchCounter(FetchCounterRef ref, String id) async {
  // Simulating a fetch operation
  await Future.delayed(const Duration(seconds: 1));
  return Counter(id);
}

@Riverpod(keepAlive: false, dependencies: [counter])
Future<Counter> detailScreenCounter(
    DetailScreenCounterRef ref, String id) async {
  ref.onDispose(() {
    print('dispose deteail');
  });
  print('exsit ${ref.exists(fetchCounterProvider(id))}');
  if (ref.exists(fetchCounterProvider(id))) {
    // Expecting this to be true consistently for keepAlive provider after reopen detailScreen
    return ref.watch(fetchCounterProvider(id).future);
  }
  final counter = ref.watch(fetchCounterProvider(id).future);
  print('exsit ${ref.exists(fetchCounterProvider(id))}');
  return Counter('default');
}

class Counter {
  final String id;
  Counter(this.id);
}

Expected behavior
The ref.exists(fetchCounterProvider(id)) should consistently return true for a provider that is marked as keepAlive: true, independent of the dependencies in the consuming provider.

Actual Behavior

  • When detailScreenCounterProvider does not have dependencies, or when dependencies are empty (dependencies: []), ref.exists(fetchCounterProvider(id)) returns true as expected for a keepAlive: true provider.

  • However, when detailScreenCounterProvider has a specific dependency (e.g., [counter]), and this dependency is overridden with ProviderScope as shown below, ref.exists(fetchCounterProvider(id)) returns `false:

return ProviderScope(
  overrides: [
    counterProvider.overrideWithValue(counter),
  ],
  child: CounterDetailScreen(counterId: id),
);

This unexpected behavior occurs despite fetchCounterProvider being marked as keepAlive: true.

@ykaito21 ykaito21 added bug Something isn't working needs triage labels Nov 20, 2023
@rrousselGit
Copy link
Owner

Duplicate of #2177

@rrousselGit rrousselGit marked this as a duplicate of #2177 Nov 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working needs triage
Projects
None yet
Development

No branches or pull requests

2 participants