Search code examples
flutterdartriverpod

How can I prevent auto-disposal when executing ref.read within initState, button.onTap in Flutter?


I've just started learning Riverpod. I am trying to use FutureProvider with Riverpod to execute time-consuming processes such as initState and Button's onTap. I don't want to keep this FutureProvider unless it's needed, so I've added autoDisposed, but if I do that, it will be immediately disposed when I run ref.read in initState or onTap. I expect that it will not be disposed until the processing is finished, but how should I fix this?

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

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

final dataProvider =
    FutureProvider.autoDispose.family<int, int>((ref, base) async {
  ref.onDispose(() {
    print('onDispose');
  });
  await Future.delayed(const Duration(seconds: 3));
  return base * 2; // just an example; actually retrieved from the server.
});

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

  @override
  ConsumerState<Sample> createState() => _SampleState();
}

class _SampleState extends ConsumerState<Sample> {
  int _sampleResult = 0;

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

    WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
      ref.read(dataProvider(2)).when(
          data: (value) {
            setState(() {
              _sampleResult = value;
            });
            print(value);
          },
          error: (error, stackTrace) {
            print(error);
          },
          loading: () {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        Text('$_sampleResult'),
        TextButton(
            onPressed: () {
              ref.read(dataProvider(_sampleResult)).when(
                    data: (value) {
                      setState(() {
                        _sampleResult = value;
                      });
                    },
                    error: (error, stackTrace) {
                      _sampleResult -= 10;
                    },
                    loading: () {},
                  );
            },
            child: const Text('test button'))
      ],
    );
  }
}

Solution

  • You do not have to declare the state variable _sampleResult and then initialise it in the initState. Instead, you simply use ref.watch(futureProvider which returns an AsyncValue<YourDataType> and use when or the new pattern matching to display the data/loading/error state accordingly.

    As long as your provider is being watched/listened, it will not be disposed.

    If you want to mutate the state, you could use Notifier/AsyncNotifier and NotifierProvider/AsyncNotifierProvider.

    I have included an example below.

    class DataNotifier extends FamilyAsyncNotifier<int, int> {
      @override
      FutureOr<int> build(int arg) async {
        ref.onDispose(() => print('onDispose'));
        await Future<void>.delayed(const Duration(seconds: 3));
        return arg * 2;
      }
    
      Future<void> increment() async {
        // there is a handy update method in AsyncNotifier which let you mutate the state without dealing with the loading/error state
        await update((value) => value + 1); 
      }
    }
    
    final dataProvider = AsyncNotifierProviderFamily<DataNotifier, int, int>(DataNotifier.new);
    
    class Sample extends ConsumerWidget {
      const Sample({super.key});
    
      @override
      Widget build(BuildContext context, WidgetRef ref) {
        // watch your provider inside build
        final data = ref.watch(dataProvider(10));
        return data.when(
          data: (value) => Row(
            children: [
              Text('$value'),
              TextButton(
                // use ref.read(yourProvider.notifier).method() to mutate your provider's state
                onPressed: () async => ref.read(dataProvider(10).notifier).increment(),
                child: const Text('test button'),
              ),
            ],
          ),
          loading: () => const CircularProgressIndicator(),
          error: (error, stackTrace) => Text('$error'),
        );
      }
    }