riverpod icon indicating copy to clipboard operation
riverpod copied to clipboard

onCancel get unexpectedly called when the listener uses .select

Open AhmedLSayed9 opened this issue 1 year ago • 3 comments

Describe the bug When a listener to a provider is using .select and that listener rebuilds, ref.onCancel for the dependent provider is getting called.

To Reproduce

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

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

final fooProvider = Provider.autoDispose((ref) {
  // Using .select here let onCancel being unexpectedly called
  ref.watch(barProvider.select((v) => v));
});

final barProvider = Provider.autoDispose((ref) {
  ref.onCancel(() {
    print('onCancel called');
  });
});

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(fooProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Riverpod example'),
      ),
      body: Container(),
      // Press the button to test the behavior
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.invalidate(fooProvider),
        child: const Icon(Icons.add),
      ),
    );
  }
}

Expected behavior I expect using .select or not to act the same for ref.onCancel behavior.

AhmedLSayed9 avatar Oct 15 '23 13:10 AhmedLSayed9

The following example might help spotting the issue. If the widget uses ref.listen (I guess it's related to .select in the previous sample), and that widget rebuilds, onCancel will get unexpectedly called. Using ref.watch instead of ref.listen work as expected.

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

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

final barProvider = Provider.autoDispose((ref) {
  ref.onCancel(() {
    print('onCancel called');
  });
});

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorSchemeSeed: Colors.blue,
        useMaterial3: true,
      ),
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {

  const MyHomePage({
    Key? key
  }) : super(key: key);

  @override
  ConsumerState<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends ConsumerState<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    // Using ref.watch will not get onCancel called.
    ref.listen(barProvider,(_,__){});
    
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

AhmedLSayed9 avatar Oct 15 '23 13:10 AhmedLSayed9

Sounds more like a ref.watch bug when not using select

It calling onCancel makes sense, since the last listener was removed for a bit.

rrousselGit avatar Oct 15 '23 18:10 rrousselGit

It calling onCancel makes sense, since the last listener was removed for a bit.

hmmm, I was expecting onCancel not to be triggered if the last listener "widget/provider" just rebuilds.

This will totally invalidate the solution offered at https://github.com/rrousselGit/riverpod/issues/1329#issuecomment-1254137017 "combining ref.onCancel with ref.keepAlive()" as any rebuild to the widget will dispose the provider and break things.

AhmedLSayed9 avatar Oct 15 '23 18:10 AhmedLSayed9

The current behaviour is expected. So closing

rrousselGit avatar Mar 01 '24 22:03 rrousselGit

@rrousselGit Is it expected for onCancel to be called with each provider rebuild?

AhmedLSayed9 avatar Mar 10 '24 11:03 AhmedLSayed9

This is something that can happen. Your code should be resilient to it.

I'll look for improvements on that area, but that's not a bug

rrousselGit avatar Mar 10 '24 12:03 rrousselGit

Well, the different behavior of onCancel when using/not using .select is a bit confusing.

I hardly spotted that behavior change when I added .select to some code in my project and it acted differently.

AhmedLSayed9 avatar Mar 10 '24 12:03 AhmedLSayed9

You can spot that by running the sample with ref.watch(barProvider.select((v) => v)); vs ref.watch(barProvider);.

AhmedLSayed9 avatar Mar 10 '24 12:03 AhmedLSayed9

It happens with any modifiers, so with .future & co too

rrousselGit avatar Mar 10 '24 12:03 rrousselGit