rrousselGit / riverpod

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

Conditional ref.watch causes disposal and rebuild of watched provider on every read #3239

Closed BenjiFarquhar closed 8 months ago

BenjiFarquhar commented 8 months ago

Describe the bug When I use a conditional ref.watch, the watched provider is disposed of and rebuilt every time the ref.watch runs. Watching it without a conditional correctly only builds the provider once.

To Reproduce, run this code

import 'dart:math';

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

part 'main.g.dart';

@riverpod
class Accumulator extends _$Accumulator {

  @override
  int build() {
    return 0;
  }

  accumulate() {
    state++;
  }
}

@riverpod
class UseProviderOne extends _$UseProviderOne {

  @override
  bool build() {
    ref.watch(accumulatorProvider);
    if (Random().nextBool()) {
      return ref.watch(providerOneProvider);
    } else {
      return ref.watch(providerTwoProvider);
    }
  }

  updateState() {
    ref.read(accumulatorProvider.notifier).accumulate();
    state = Random().nextBool();
  }
}

@riverpod
bool providerOne(ProviderOneRef ref) {
  print('rebuilding provider one');
  return true;
}

@riverpod
bool providerTwo(ProviderTwoRef ref) {
  print('rebuilding provider two');
  return false;
}

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends ConsumerStatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

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

class _MyHomePageState extends ConsumerState<MyHomePage> {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: const Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => ref.watch(useProviderOneProvider.notifier).updateState(),
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Expected behavior When tapping the button repeatedly, it should only print "rebuilding provider one" and "rebuilding provider two" once each.

Actual behavior It prints them many times after many button taps.

When we change build to this it works as expected:

  @override
  bool build() {
    final p1 = ref.watch(providerOneProvider);
    final p2 = ref.watch(providerTwoProvider);
    ref.watch(accumulatorProvider);

    if (Random().nextBool()) {
      return p1;
    } else {
      return p2;
    }
  }

Versions:

flutter_riverpod: ^2.4.9 riverpod_generator: ^2.3.9 riverpod_annotation: ^2.3.3 build_runner: ^2.4.6

rrousselGit commented 8 months ago

That looks normal to me. The provider wasn't listened so got disposed. Reading it again after it was disposed recomputes it.

BenjiFarquhar commented 8 months ago

I was unaware this was expected. I tried to do lazy loading of providers by watching them in a switch statement in the build method. But I must watch them upfront to prevent them from recomputing.

Cheers.