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

Bad state: Tried to use StateNotifier after dispose was called. #496

Closed
SalahAdDin opened this issue May 28, 2021 · 17 comments
Closed

Bad state: Tried to use StateNotifier after dispose was called. #496

SalahAdDin opened this issue May 28, 2021 · 17 comments
Labels
question Further information is requested

Comments

@SalahAdDin
Copy link

Describe the bug
We are using a StateNotifier to handle our application's workflow; in this case, we are handling the signin workflow, but at finsihing the process, or along itself, the StateNotifier is disposes before to finish is workflow:

[ +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): 

To Reproduce
This is the StateNotifierProvider:

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",
);

This is the function that loges in the user and gives the bug:

final Map<String, dynamic> rawUser = await authRepository.signIn(
          identifier: username, password: password) as Map<String, dynamic>;
      User sessionUser = User.fromJson(rawUser);
      if (sessionUser.confirmed != false) {
        tokenController.state = sessionUser.token!;
        final Profile sessionUserProfile =
            await profileRepository.findByUserId(sessionUser.id);
        sessionUser = sessionUser.copyWith(profile: sessionUserProfile);
      }
      await dataStore.writeUserProfile(sessionUser);
      userController.state = sessionUser;
      state = const AuthState.loggedIn();

Specifically the last one.

It follows the workflow correctly:

  1. It gets get user and transform it to the model.
  2. As the user is confirmed, it nows fetch the user's profile, parses it and adds it to the fetched user.
  3. It writes the user on the local storage.
  4. It sets the current provided user as the fetched user.
  5. It never returns anything because it breaks here.
[   +1 ms] I/flutter ( 8521): [Settings Provider] value: AsyncValue<Never>.loading()
[   +5 ms] I/flutter ( 8521): [Auth Repository Provider] value: Instance of 'GraphQLAuthRepository'
[        ] I/flutter ( 8521): Token: 
[   +3 ms] I/flutter ( 8521): [GraphQL Auth Client Provider] value: Instance of 'GraphQLClient'
[        ] I/flutter ( 8521): [Profile Repository Provider] value: Instance of 'GraphQLProfileRepository'

[   +6 ms] I/flutter ( 8521): [Authentication Notifier Provider.notifier] value: Instance of 'AuthNotifier'
[        ] I/flutter ( 8521): [Authentication Notifier Provider] value: AuthState.loading()

[ +171 ms] E/flutter ( 8521): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Bad state: Tried to use AuthNotifier after `dispose` was called.
[   +1 ms] E/flutter ( 8521): 
[        ] E/flutter ( 8521): Consider checking `mounted`.
[        ] E/flutter ( 8521): 
[        ] E/flutter ( 8521): #0      StateNotifier._debugIsMounted.<anonymous closure> (package:state_notifier/state_notifier.dart:128:9)
[   +1 ms] E/flutter ( 8521): #1      StateNotifier._debugIsMounted (package:state_notifier/state_notifier.dart:135:6)
[        ] E/flutter ( 8521): #2      StateNotifier.state= (package:state_notifier/state_notifier.dart:155:12)
[        ] E/flutter ( 8521): #3      AuthNotifier.signIn (package:thesis_cancer/features/auth/application/auth.notifier.dart:84:7)
[        ] E/flutter ( 8521): <asynchronous suspension>
[        ] E/flutter ( 8521): #4      _LoginCardState._submit (package:flutter_login/src/widgets/auth_card.dart:503:15)
[        ] E/flutter ( 8521): <asynchronous suspension>
[   +1 ms] E/flutter ( 8521): 

[  +15 ms] I/flutter ( 8521): [User Entity Provider] value: User(id: 40, email: alagunasalahaddin@live.com, username: luis, profile: Profile(firstName: null, lastName: null, profilePhoto: null, role: UserRole.PILOT, phoneNumber: null, hasSeenTutorial: false, hasSeenIntroductoryVideo: false, bio: null), token: , confirmed: true, isLoggedIn: null, surveyResults: null, posts: null, comments: null, likes: null)
[  +85 ms] I/flutter ( 8521): [Settings Provider] value: AsyncValue<Settings>.data(value: Settings(introductoryVideo: UploadFile(alternativeText: , caption: , url: https://cancer-strapi-s3.s3.us-east-2.amazonaws.com/Record_select_area_20210520132303_dfc42dded9.mp4, width: null, height: null), registeringSurvey: 1, surveySchedules: [SurveySchedule(iterations: 3, step: 2, survey: 1), SurveySchedule(iterations: 3, step: 4, survey: 1)], darkTheme: false))
[+26842 ms] W/l.thesis_cance( 8521): JNI critical lock held for 20.970ms on Thread[1,tid=8521,Runnable,Thread*=0xeb834e00,peer=0x723271f0,"main"]

Expected behavior
After finishing the process, it should finish the function and leave us to continue with the next screen.

@SalahAdDin SalahAdDin added bug Something isn't working needs triage labels May 28, 2021
@rrousselGit
Copy link
Owner

It sounds like this is a bug in your logic instead

It is likely that your StateNotifier was rebuilt during an await, before of the ref.watch.

Consider passing ProviderReference to your StateNotifier and use ref.read instead.

@rrousselGit rrousselGit added question Further information is requested and removed bug Something isn't working needs triage labels May 28, 2021
@SalahAdDin
Copy link
Author

SalahAdDin commented May 28, 2021

The specifical problem comes from: sessionUser = sessionUser.copyWith(profile: sessionUserProfile); if i avoid this line, there is no any problem on the logic.
Trying this one with:

final User fetchedUser = User.fromJson(rawUser);
      User sessionUser;
      if (fetchedUser.confirmed != false) {
        tokenController.state = fetchedUser.token!;
        final Profile sessionUserProfile =
            await profileRepository.findByUserId(fetchedUser.id);
        sessionUser = fetchedUser.copyWith(profile: sessionUserProfile);
      } else {
        sessionUser = fetchedUser;
      }

It still does not work.

Why?

@rrousselGit
Copy link
Owner

The problem is not those lines. It is that those lines are placed after an await

Your StateNotifier was disposed & recreating during the await

@SalahAdDin
Copy link
Author

Using ref.read from the provider definition does not work:

final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(
    authRepository: ref.read(authRepositoryProvider),
    dataStore: ref.watch(dataStoreRepositoryProvider),
    profileRepository: ref.watch(profileRepositoryProvider),
    tokenController: ref.watch(tokenProvider.notifier),
    userController: ref.watch(userEntityProvider.notifier),
  ),
  name: "Authentication Notifier Provider",
);

Now, that authRepository needs to be a repository instead of a Provider Reference, in that case, what should i do?

@julienlebren
Copy link
Contributor

julienlebren commented May 31, 2021

Remi is right, I had exactly the same problem today.

A StateNotifierProvider watches an object, then it updates this object. The function cannot reach its end because the StateNotifierProvider is rebuilt because of the "watching" of the object and it triggered exactly the same error message that you got.

This is a kind of "loop" where something watches an object but also updates this object so get rebuilt. It's a matter of use and not a bug 😉

I think the problem is here:

userController.state = sessionUser;

You update the state of userController whereas your StateNotifier is watching userController.
So, especially at this moment, your StateNotifier is rebuilt and the end of the function is never executed.

@SalahAdDin
Copy link
Author

Remi is right, I had exactly the same problem today.

A StateNotifierProvider watches an object, then it updates this object. The function cannot reach its end because the StateNotifierProvider is rebuilt because of the "watching" of the object and it triggered exactly the same error message that you got.

This is a kind of "loop" where something watches an object but also updates this object so get rebuilt. It's a matter of use and not a bug

I think the problem is here:

userController.state = sessionUser;

You update the state of userController whereas your StateNotifier is watching userController.
So, especially at this moment, your StateNotifier is rebuilt and the end of the function is never executed.

Actually, the problem is here: tokenController.state = sessionUser.token!;.

@julienlebren how did you solve the problem them?

@julienlebren
Copy link
Contributor

julienlebren commented Jun 1, 2021

Actually, the problem is here: tokenController.state = sessionUser.token!;.

Yep that's exactly the same kind of line, it updates a state which is watched by your StateNotifier, causing its whole rebuild.

In fact, you don't need to watch your tokenController here:

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(
    authRepository: ref.read(authRepositoryProvider),
    dataStore: ref.watch(dataStoreRepositoryProvider),
    profileRepository: ref.watch(profileRepositoryProvider),
    tokenController: ref.watch(tokenProvider.notifier),
    userController: ref.watch(userEntityProvider.notifier),
  ),
  name: "Authentication Notifier Provider",
);

You are just passing your tokenController to your AuthNotifier in order to update your tokenController, right?

There are many solutions to this, one of the solutions would be to pass ref.read to your AuthNotifier:

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(ref.read),
  name: "Authentication Notifier Provider",
);

Your AuthNotifier becomes:

class AuthNotifier extends StateNotifier<AuthState> {
  AuthNotifier(
    this.read,
  ) : super(AuthState.initial()); // put your initial state here

  final Reader read;
}

Then in your login method:

final User fetchedUser = User.fromJson(rawUser);
      User sessionUser;
      if (fetchedUser.confirmed != false) {
        read(tokenController).state = fetchedUser.token!;
        final Profile sessionUserProfile =
            await read(profileRepository).findByUserId(fetchedUser.id);
        sessionUser = fetchedUser.copyWith(profile: sessionUserProfile);
      } else {
        sessionUser = fetchedUser;
      }

Aditionnally, in your initial code, please note that you should not use ref.read inside the bpdy of your provider, [as mentioned in the documentation here(https://riverpod.dev/docs/concepts/combining_providers)].

@SalahAdDin
Copy link
Author

  authRepository: ref.read(authRepositoryProvider),
    dataStore: ref.watch(dataStoreRepositoryProvider),
    profileRepository: ref.watch(profileRepositoryProvider),
    tokenController: ref.watch(tokenProvider.notifier),
    userController: ref.watch(userEntityProvider.notifier),

What's about ProviderReference?

And, read will replace the other dependencies we have, right?

The workflow right now, as we understand is:

  1. Login function does ovewrite the token.
  2. Token change does trigger a rebuilt onto the GraphQL client.
  3. In turn, this client does trigger a re-built on the Authentication Repository.
  4. Finally, this repository triggers a re-built on the Auth Notifier.

This is the workflow who gives the issue now.

@julienlebren
Copy link
Contributor

julienlebren commented Jun 1, 2021

What's about ProviderReference?

ProviderReference can be a solution, some part of the logic would move in the ProviderReference listener inside your Widget. It would need some changes to your AuthState class to return the sessionUser:

class AuthState {
  const factory AuthState.loggedIn(User user) = _Authed;
}

Then in your view:

class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ProviderListener<AuthState>(
      provider: authNotifierProvider,
      onChange: (context, state) {
        state.maybeWhen(
           loggedIn: (user) { 
              context.read(tokenController).state = user.token!;
              context.read(userController).state = user;
           },
           orElse: () => null,
        }
      },
      child: ...,
    );
  }
}

And, read will replace the other dependencies we have, right?

read will not replace anything, it will just... read 😉

The first solution I gave is the best imho, since you have a bunch of stuff to do inside your notifier, passing a Reader to access the other providers is the best thing to do.

@SalahAdDin
Copy link
Author

Actually, the problem is here: tokenController.state = sessionUser.token!;.

Yep that's exactly the same kind of line, it updates a state which is watched by your StateNotifier, causing its whole rebuild.

In fact, you don't need to watch your tokenController here:

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(
    authRepository: ref.read(authRepositoryProvider),
    dataStore: ref.watch(dataStoreRepositoryProvider),
    profileRepository: ref.watch(profileRepositoryProvider),
    tokenController: ref.watch(tokenProvider.notifier),
    userController: ref.watch(userEntityProvider.notifier),
  ),
  name: "Authentication Notifier Provider",
);

You are just passing your tokenController to your AuthNotifier in order to update your tokenController, right?

There are many solutions to this, one of the solutions would be to pass ref.read to your AuthNotifier:

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(ref.read),
  name: "Authentication Notifier Provider",
);

Your AuthNotifier becomes:

class AuthNotifier extends StateNotifier<AuthState> {
  AuthNotifier(
    this.read,
  ) : super(AuthState.initial()); // put your initial state here

  final Reader read;
}

Then in your login method:

final User fetchedUser = User.fromJson(rawUser);
      User sessionUser;
      if (fetchedUser.confirmed != false) {
        read(tokenController).state = fetchedUser.token!;
        final Profile sessionUserProfile =
            await read(profileRepository).findByUserId(fetchedUser.id);
        sessionUser = fetchedUser.copyWith(profile: sessionUserProfile);
      } else {
        sessionUser = fetchedUser;
      }

Aditionnally, in your initial code, please note that you should not use ref.read inside the bpdy of your provider, [as mentioned in the documentation here(https://riverpod.dev/docs/concepts/combining_providers)].

Cool, We tried this one, and, it seems to be working corretly man, thank you!

@wisnuwiry
Copy link

The problem is not those lines. It is that those lines are placed after an await

Your StateNotifier was disposed & recreating during the await

How to fix this when stateNotifier needs an async function?

@rrousselGit

@icodeyou
Copy link

@wisnuwiry

How to fix this when stateNotifier needs an async function?

I also had a StateNotifier needing to be initialized with an async function, juste like this :
#57 (comment)

What I did was to check that my StateNotifier was mounted before changing its state :

Future<int> fetch() async =>
    Future.delayed(const Duration(seconds: 1)).then((value) => 42);

class Whatever extends StateNotifier<AsyncValue<int>> {
  Whatever(): super(const AsyncValue.loading()) {
    _fetch();
  }

  Future<void> _fetch() async {
    state = const AsyncValue.loading();
    try {
      final result = await fetch();
      if (mounted) {
        state = AsyncData(result);
      }
    } catch (e, s) {
      state = AsyncError(e, s);
    }
  }
}

@wisnuwiry
Copy link

@wisnuwiry

How to fix this when stateNotifier needs an async function?

I also had a StateNotifier needing to be initialized with an async function, juste like this : #57 (comment)

What I did was to check that my StateNotifier was mounted before changing its state :

Future<int> fetch() async =>
    Future.delayed(const Duration(seconds: 1)).then((value) => 42);

class Whatever extends StateNotifier<AsyncValue<int>> {
  Whatever(): super(const AsyncValue.loading()) {
    _fetch();
  }

  Future<void> _fetch() async {
    state = const AsyncValue.loading();
    try {
      final result = await fetch();
      if (mounted) {
        state = AsyncData(result);
      }
    } catch (e, s) {
      state = AsyncError(e, s);
    }
  }
}

Thank you @icodeyou , I have solved the issue I found in a similar way to yours.
So in each asynchronous function, before there is a state change, it must be checked mounted first.

@Sagar-KC-3082
Copy link

What I learned from my issue was : If you are using a stateNotifierProvider.autoDispose<>... then make sure that it is watched somewhere because the autoDispose ensures that the provider is created only when it is used somewhere in screen(is watched somewhere)....so use final temp = useProvider(providerName)....
Or.. simply remove the autoDispose from the provider if you don't want to watch the provider...

@dongnqda
Copy link

dongnqda commented Apr 7, 2023

@icodeyou Thank you for the solution, but do you know any way we could stop completely the function that is running without have to manually check mounted ?
Cause a function may has state = ... calling multiple times -> We have to check every state =... call.

Or there is a way to stop running function right at the moment mounted turn false ? Cause after state = ..... There are maybe some unnecessary expensive operation that no longer need to be called.

@quyenlv-unicloud
Copy link

@dongnqda if dont want check multiple times you can override?

  @override
  set state(T value) {
    if (!mounted) return;
    super.state = value;
  }

@FeDev9
Copy link

FeDev9 commented Jan 19, 2024

Hi!
I have the same issue of @SalahAdDin,
this is the error

StateError (Bad state: Tried to use HomeStudiosNotifier after dispose was called.
 Consider checking mounted.
)

StateNotifierProvider:

final homeStudiosProvider =
    StateNotifierProvider<HomeStudiosNotifier, HomeStudiosState>((ref) {


  ref.watch(authProvider);
  return HomeStudiosNotifier(
      ref: ref);
});

and StateNotifier

class HomeStudiosNotifier extends StateNotifier<HomeStudiosState> {
  HomeStudiosNotifier({
    required this.ref,

  }) : super(HomeStudiosState.loading()) {
    _getStudios(GetStudios());
  }

  final Ref ref;

  dynamic mapEventToState(HomeStudiosEvent event) {
    return event.map(
      getStudios: _getStudios,
      requestMembership: _requestMembership,
    );
  }

  Future<Unit> _getStudios(GetStudios data) async {
     state = HomeStudiosState.loading();

    final res = await ref.read(studiosRepositoryProvider).getStudios();

    res.fold(
.
.
.


There is the only authProvider await before the initialization of this provider, but start after loading auth data.
Thanks!

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

No branches or pull requests

9 participants