rrousselGit / riverpod

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

Issue With ChangeNotifier Listeners not being called #346

Closed bbrosius closed 3 years ago

bbrosius commented 3 years ago

Describe the bug I'm writing an error handler for rest API requests I'm running into a weird problem with ChangeNotifierProvider. For some reason the notifyListeners() call only seems to work if I make the call with the context.read(). Both ProviderContainer and ref.read don't work.

Here is how I'm declaring my errorHandler providers.

final errorHandlerProvider = ChangeNotifierProvider((ref) => ErrorHandler());

final errorProvider = Provider<OneVueError>((ref) {
  return ref.watch(errorHandlerProvider).error;
});

The idea is that my UI code will listen to errorProvider to display error messages after rest calls. And the API side will use errorHandlerProvider to process errors.

I may an API Call like this:

try {
      await container
          .read(loginAPIProvider)
          .authenticate(AuthenticateRequest(email, password))
          .then((loginResponse) {
        if (loginResponse != null && loginResponse.token.isNotEmpty) {
          saveToken(loginResponse.token);
          _authenticationResult = different == true
              ? AuthenticationResult.different
              : AuthenticationResult.same;
          _currentUser = UserInfo(_authenticationResult,
              User("AuthenticatedUser", "AuthenticationPassword"));
          notifyListeners();
          return true;
        } else {
          return false;
        }
      });
    } on DioError catch (e) {
      print("caught error");
      container.read(errorHandlerProvider).testError();
    }

Then the test error message looks like this

void testError() {
    print("test error");
    _oneVueError = OneVueError(ErrorType.unknown, "test error");
    notifyListeners();
  }

Test error is called every time. But my errorProvider break point is never hit, and the UI is never updated. In the code posted above I used a ProviderContainer as a test. I had previously set it up the I think correct way of using ref.read in the provider declaration for the service where the call is made. Neither of those worked. The only time errorProvider is updated is when I hacked up my code to pass the BuildContext from the UI all the way into the service and then called context.read(errorHandlerProvider).testError().

Any ideas how I can get this to work correctly without passing context through into my business logic, or why this is causing a problem?

TimWhiting commented 3 years ago

Can you show what you were doing before with ref.read which is the way you should be doing this. ProviderContainer should not be used like this.

bbrosius commented 3 years ago

Sure I'll do my best to pull out the relevant sections.

So I have a Service class called authService that is making the API call. So the authService provider is created like this.

final authServiceProvider = ChangeNotifierProvider(
    (ref) => AuthService(ref.watch(errorHandlerProvider)));

Declarations of AuthService are as shown

class AuthService with ChangeNotifier {
  UserInfo _currentUser;
  AuthenticationResult _authenticationResult;
  final ErrorHandler errorHandler;
  // for R&D purpose
  Timer timer;

  AuthService(this.errorHandler);

This uses the provider declaration for ErrorHandler in my previous post.

Inside the authService it catches the DioError and calls testError.

} on DioError catch (e) {
      print("caught error");
      errorHandler.testError();
    }

Here is the full error handler class with test error.

final errorHandlerProvider = ChangeNotifierProvider((ref) => ErrorHandler());

final errorProvider = Provider<OneVueError>((ref) {
  return ref.watch(errorHandlerProvider).error;
});

class ErrorHandler extends ChangeNotifier {
  OneVueError _oneVueError;

  OneVueError get error => _oneVueError;
  void handleNetworkError(DioError error) {
    print(
        "Got error : ${error.response?.statusCode} -> ${error.response?.statusMessage}");

    final container = ProviderContainer();

    if (error.type == DioErrorType.RESPONSE) {
      //Redirect to login on unauthorized status
      if (error.response?.statusCode == 401) {
        container.read(authServiceProvider).deauthenticate();
      } else {
        _oneVueError = ErrorFactory.errorForNetworkRequest(
            error.response.statusCode, error.request.path);
      }
    } else {
      //Log dio error with Crashlytics?

    }

    notifyListeners();
  }

  void testError() {
    print("Testing error handling");
    _oneVueError = OneVueError(ErrorType.unknown, "Test error");
    notifyListeners();
  }
}

Right now I'm only using the test error method would eventually move to the other method.

From there I would expect my errorProvider to update.

final errorProvider = Provider<OneVueError>((ref) {
  return ref.watch(errorHandlerProvider).error;
});

But that never happens. As mentioned before if I either pass through the buildcontext or watch the errorHandlerProvider in the view class and pass it all the way through to the service it works. In that case it would look more like this.

} on DioError catch (e) {
      print("caught error");
      context.watch(errorHandlerProvider).testError();
    }

It updates the errorProvider as expected. But obviously, I would like to avoid passing around the BuildContext which is one of the reasons for using riverpod. Also eventually I would like to hook this up as an interceptor with dio at which point I won't be able to pass in the buildContext since that class isn't directly called by the view.

TimWhiting commented 3 years ago
import 'dart:async';

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Test(),
    );
  }
}

class Test extends ConsumerWidget {
  const Test();
  @override
  Widget build(BuildContext context, ScopedReader watch) {
    return Scaffold(
      body: ProviderListener(
        provider: errorProvider,
        onChange: (c, err) {
          print('Showing snackbar');
          ScaffoldMessenger.of(c).showSnackBar(
            SnackBar(
              content: Text('Error $err'),
            ),
          );
        },
        child: Center(
          child: Column(
            children: [
              Text(''),
              ElevatedButton(
                child: Text('Log in'),
                onPressed: () {
                  context.read(authServiceProvider).authenticate();
                },
              )
            ],
          ),
        ),
      ),
    );
  }
}

final authServiceProvider = ChangeNotifierProvider(
    (ref) => AuthService(ref.watch(errorHandlerProvider)));
final errorHandlerProvider =
    ChangeNotifierProvider((ref) => ErrorHandler(ref.read));

final errorProvider = Provider<OneVueError>((ref) {
  return ref.watch(errorHandlerProvider).error;
});

class ErrorHandler extends ChangeNotifier {
  Reader read;
  ErrorHandler(this.read);
  OneVueError _oneVueError;

  OneVueError get error => _oneVueError;
  void handleNetworkError(DioError error) {
    print(
        "Got error : ${error.response?.statusCode} -> ${error.response?.statusMessage}");

    if (error.type == DioErrorType.RESPONSE) {
      //Redirect to login on unauthorized status
      if (error.response?.statusCode == 401) {
        read(authServiceProvider).deauthenticate();
      } else {
        _oneVueError = ErrorFactory.errorForNetworkRequest(
            error.response.statusCode, error.request.path);
      }
    } else {
      //Log dio error with Crashlytics?

    }

    notifyListeners();
  }

  void testError() {
    print("Testing error handling");
    _oneVueError = OneVueError(ErrorType.unknown, "Test error");
    notifyListeners();
  }
}

This seems to be working for me. Make sure you pass ref.read to ErrorHandler, and use that, and not the ProviderContainer.

TimWhiting commented 3 years ago

ProviderContainer creates a whole new and different scope and set of states that is separate from the ProviderScope in the widget tree. Always pass a ref.read into a Provided object if you anticipate needing to access other providers from it.

rrousselGit commented 3 years ago

Tim's solution is correct, so I will close this.

bbrosius commented 3 years ago

@TimWhiting Thanks for that I didn't realize the Container was creating a different scope eliminating all usage of that solved the problem. Thanks again.