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

Question about implementing of ref.listenSelf in auth.dart #14

Closed
EthanHipps opened this issue Jan 9, 2023 · 3 comments
Closed

Question about implementing of ref.listenSelf in auth.dart #14

EthanHipps opened this issue Jan 9, 2023 · 3 comments

Comments

@EthanHipps
Copy link

Hello,

I have a dumb question and apologize in advance if this isn't the correct place to ask it.

In auth.dart, the asynchronous build() calls _persistenceRefreshLogic() which then calls ref.listenSelf(). According to the Riverpod docs, ref.listen() shouldn't be called asynchronously.

Does that guidance not specifically apply to ref.listenSelf() or this usage in general (i.e. if I wanted to call ref.listen in a similar method would it be okay)?

@lucavenir
Copy link
Owner

lucavenir commented Jan 9, 2023

Hi there,

there are no dumb questions.

ref.listen shouldn't be called asynchronously

This is true. And it's also true for ref.watch.

Why tho?

That's because registering a callback which reactively listens to other providers (i.e. events) might be dangerous if performed asynchronously. Imagine a scenario in which we await for a "relatively long" time before registering such callback. By that time, that provider might have been disposed (runtime error) or - worse - it could have changed (silent bug).

This is why the docs state this shouldn't be done in a (e.g.) onPressed async event or in other State - lifecycles functions.

Luckily, when using ref.listenSelf inside our own build method this danger disappears; after the asynchronous gap, we might have the following scenarios:

  1. The async operation failed (somehow). The function interrupts (throws) and the Notifier initializes with an AsyncError
  2. The async operation succeeds: the following lines behaves just as expected
  3. While we wait for the async operation to complete, someone else tries to read our internal state. This is ok: the AsyncNotifier emits a loading value, so nothing bad can really happen
  4. While we wait for the async operation to complete, someone else tries to modify our internal state. This would be bad as much as the example above, but this can't happen: Riverpod would raise a StateError since you can't change state while initializing (I am sure you've received an error message like this before)
  5. While we wait for the async operation to complete, our Notifier gets disposed. This won't happen as authentication is basically lisened at the root of our application. Nonetheless, it can be easily investigated if listenSelf is safe with respect of this scenario with a simple reproducible example.

Nonetheless, chances are I will edit that line out: obtaining SharedPreferences is commonly done in a separated, synchronous Provider, so thanks for question. See, you can always help somebody.

@EthanHipps
Copy link
Author

Thank you very much for the response and detailed explanation. I really appreciate it!

@lucavenir
Copy link
Owner

lucavenir commented Jan 9, 2023

I wanted to further explore this behavior. Here's a full reproducible example:

import 'dart:math';

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Riverpod Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Riverpod demo here we go!'),
      routes: {'new-route': (context) => const MyWidget()},
    );
  }
}

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

  final String title;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: const <Widget>[
            Text(
                "Press the button to initialize the other screen and providers!")
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          Navigator.of(context).pushNamed('new-route');
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final a = ref.watch(stateProvider).when(
          data: (data) => "$data",
          error: (error, stackTrace) => "Error",
          loading: () => "loading...",
        );

    return Scaffold(
      body: Center(
        child: InkWell(
          onTap: () async {
            Navigator.of(context).pop();
            await Future.delayed(const Duration(seconds: 3));
            try {
              // won't work (remove try catch to see what happens)
              print("Reading 1... ${ref.read(otherProvider)}");
            } catch (e) {}
            try {
              // won't work (remove try catch to see what happens)
              print("Reading 2... ${ref.read(stateProvider)}");
            } catch (e) {}
          },
          child: Container(
            color: Colors.amberAccent,
            padding: const EdgeInsets.all(32),
            child: Text(a),
          ),
        ),
      ),
    );
  }
}

@riverpod
Future<int> state(StateRef ref) async {
  ref.onResume(() {
    print("I am alive again!");
  });
  ref.onCancel(() {
    print("I have no more listeners, I'm gonna be disposed!");
  });
  ref.onDispose(() {
    print("I have been disposed!");
  });
  final result = await Future.delayed(
    const Duration(seconds: 2),
    () => Random().nextInt(10),
  );

  ref.listenSelf((previous, next) {
    print("I have listened to myself: $next");
  });

  ref.listen(otherProvider, (previous, next) {
    print("I have listened to other: $next");
  });

  ref.keepAlive();

  return result;
}

@riverpod
Future<int> other(OtherRef ref) async {
  ref.onResume(() {
    print("[other] I am alive again!");
  });
  ref.onCancel(() {
    print("[other] I have no more listeners, I'm gonna be disposed!");
  });
  ref.onDispose(() {
    print("[other] I have been disposed!");
  });
  final result = await Future.delayed(
    const Duration(seconds: 2),
    () => Random().nextInt(10),
  );

  ref.listenSelf((previous, next) {
    print("[other] I have listened to myself");
  });

  ref.keepAlive();

  return result;
}

I invite you to test this out, and to go in and out the pushed route (press the button and then the rectangle) - and read the logs.

There's a few takeaways:

  1. Riverpod can't magically interrupt functions, so whatever happens inside the build functions or inside providers, those instructions will be executed. So listen and listenSelf are executed anyways.
  2. In this example an asynchronous ref.listen won't crash anything, but will temporarily "awaken" other and trigger the callback, which might be undesired. Then, the provider will be disposed (and its listeners removed). I might have said something incorrect about the state error above, but still, don't use listen asynchronously.
  3. As you can see ref.listenSelf isn't ever triggered in the first Provider, which is desired. So it's safe to use.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants