Reputation: 2270
We are using GraphQL to connect our backend with our mobil application. We are using Riverpod to, among other things, handle global state, dependency injection, etc.
We are using a provider to handle the GraphQL cliente without any authentication header at first while the user is not authentication (this will handle unauthenticated request like login or registering), after the user is authenticated, a token provider who provides the token is updated and it must update the client provider who is user for every repository on the application:
final StateProvider<String> tokenProvider =
StateProvider<String>((_) => '', name: "Token Provider");
final graphQLClientProvider = Provider<GraphQLClient>(
(ref) {
final String token = ref.watch(tokenProvider).state;
final Link _link = HttpLink(
'http://192.168.1.36:1337/graphql',
defaultHeaders: {
if (token != '') 'Authorization': 'Bearer $token',
});
return GraphQLClient(
cache: GraphQLCache(),
link: _link,
);
},
name: "GraphQL Client Provider",
);
There is too problems here:
[ +141 ms] E/flutter ( 8361): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Bad state: Tried to use AuthNotifier after `dispose` was called.
[ ] E/flutter ( 8361):
[ ] E/flutter ( 8361): Consider checking `mounted`.
[ ] E/flutter ( 8361):
[ ] E/flutter ( 8361): #0 StateNotifier._debugIsMounted.<anonymous closure> (package:state_notifier/state_notifier.dart:128:9)
[ ] E/flutter ( 8361): #1 StateNotifier._debugIsMounted (package:state_notifier/state_notifier.dart:135:6)
[ ] E/flutter ( 8361): #2 StateNotifier.state= (package:state_notifier/state_notifier.dart:155:12)
[ ] E/flutter ( 8361): #3 AuthNotifier.signIn (package:thesis_cancer/features/auth/application/auth.notifier.dart:84:7)
[ ] E/flutter ( 8361): <asynchronous suspension>
[ ] E/flutter ( 8361): #4 _LoginCardState._submit (package:flutter_login/src/widgets/auth_card.dart:503:15)
[ ] E/flutter ( 8361): <asynchronous suspension>
[ ] E/flutter ( 8361):
It means when the token is updated, the authentication notifier who receives the repository with depends on the provided client is updated/changes (following the chain):
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
name: 'Auth Repository Provider',
);
final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
(ref) => AuthNotifier(
authRepository: ref.watch(authRepositoryProvider),
dataStore: ref.watch(dataStoreRepositoryProvider),
profileRepository: ref.watch(profileRepositoryProvider),
tokenController: ref.watch(tokenProvider.notifier),
userController: ref.watch(userEntityProvider.notifier),
),
name: "Authentication Notifier Provider",
);
The notification function who breaks at the conditional(if):
Future<String?> signIn({
required String username,
required String password,
}) async {
try {
final Map<String, dynamic> rawUser = await authRepository.signIn(
identifier: username, password: password) as Map<String, dynamic>;
final User sessionUser = User.fromJson(rawUser);
if (sessionUser.confirmed != false) {
tokenController.state = sessionUser.token!; <-- Bug begins here
}
await dataStore.writeUserProfile(sessionUser);
userController.state = sessionUser;
state = const AuthState.loggedIn();
} on LogInFailureByBadRequest {
return "E-posta veya şifre geçersiz.";
} on LogInFailure catch (error) {
return error.toString();
}
}
Following the comments on the issue we opened, we added the ProviderReference
to our notifier (following this question) but it does not work:
AuthNotifier(
this._ref, {
required this.authRepository,
required this.profileRepository,
required this.dataStore,
required this.tokenController,
required this.userController,
}) : super(const AuthState.loading());
final ProviderReference _ref;
final ProfileRepository profileRepository;
final AuthRepository authRepository;
final DataStoreRepository dataStore;
final StateController<User?> userController;
final StateController<String> tokenController;
At this point, we don't understand what's the problem here.
UPDATE
We removed this changes both in the StateNotifier and its provider.
Getting the client at repository by read does not avoid the error:
What's the proper way to handle the authorization token and the GraphQL client with Riverpod?
Thank you.
Upvotes: 2
Views: 4502
Reputation: 344
The problem is the authRepositoryProvider
:
final authRepositoryProvider = Provider<AuthRepository>(
(ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
name: 'Auth Repository Provider',
);
In this fragment, the authRepositoryProvider
is using a GraphQLClient
without token. When the token is updated, the graphQLClientProvider
state is well updated, but the authRepositoryProvider
isn't because you are using ref.read
avoiding the rebuild of the authRepositoryProvider
state. This implies that the authRepositoryProvider
knows a disposed (and unmounted) state of graphQLClientProvider
and that's why the error message.
There are 2 solutions:
ref.watch
:final authRepositoryProvider = Provider<AuthRepository>(
(ref) => GraphQLAuthRepository(client: ref.watch(graphQLClientProvider)),
name: 'Auth Repository Provider',
);
ref.read
as parameter:final authRepositoryProvider = Provider<AuthRepository>(
(ref) => GraphQLAuthRepository(client: ref.read),
name: 'Auth Repository Provider',
);
class AuthRepository {
AuthRepository(this.read)
final Reader read;
GraphQLClient get client => read(graphQLClientProvider);
}
Be care about using read
and watch
, the first one does not react to the changes but could be used anywhere, while the second one reacts to a provider's state update but only can be used in another provider constructor or in a widget that provides access to a watch
or ref.watch
depending on the version of Riverpod.
In addition, there is another problem in your code:
final User sessionUser = User.fromJson(rawUser);
if (sessionUser.confirmed != false) {
// In this moment, the token is updated,
// then the QrahpQLClient is updated,
// then the AuthNotifier is recreated,
// i.e the current is disposed
// and now becomes the previous
tokenController.state = sessionUser.token!; <-- Bug begins here
}
await dataStore.writeUserProfile(sessionUser);
userController.state = sessionUser;
// In this moment you are updating the state of a disposed notifier
state = const AuthState.loggedIn();
To provide a solution it is necessary to define which elements will make rebuild the AuthNotifer.state
instead of recreating the notifier.
You must consider access to another notifiers bases on the ref.read
and manually create the subscriptions in the StateController constructor.
In the future, when riverpod 1.0 will be released, this issue will be easy with ref.listen
but now you need to rewrite your code. You can take inspiration in the bloc-to-bloc
communication gide: https://bloclibrary.dev/#/architecture?id=bloc-to-bloc-communication
Upvotes: 3
Reputation: 599
i think the error is in your AuthNotifier:
if (sessionUser.confirmed != false) {
tokenController.state = sessionUser.token!; <-- you are updating a dependencie of your actual AuthNotifier object
}
await dataStore.writeUserProfile(sessionUser); <-- during the await a new object is created and the actual instance is disposed so this line will throw
Upvotes: 1