Search code examples
flutterdartlistenerriverpodflutter-go-router

Problem using Riverpod Code Generation in Dart (addListener)


I am doing a project with riverpod with annotations after I did the same project without annotations. I have tried to adapt all the providers that I have in the first project, but I am having a problem when accessing the _authNotifier.addListener since it tells me that it is not defined.

I'm not sure what is going on, so it will be helpful y you can help me!

This is my first project's code and it works perfect

**auth_provider.dart**

final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
  final authRepository = AuthRepositoryImpl();
  final keyValueStorageService = KeyValueStorageServiceImpl();

  return AuthNotifier(
    authRepository: authRepository,
    keyValueStorageService: keyValueStorageService,
  );
});

class AuthNotifier extends StateNotifier<AuthState> {
  final AuthRepository authRepository;
  final KeyValueStorageService keyValueStorageService;

  AuthNotifier({
    required this.authRepository,
    required this.keyValueStorageService,
  }) : super(AuthState()) {
    checkAuthStatus();
  }

  Future<void> loginUser(String email, String password) async {
    try {
      final user = await authRepository.login(email, password);
      _setLoggedUser(user);
    } on CustomError catch (e) {
      logout(e.message);
    } catch (e) {
      logout('Error no controlado');
    }
  }

  void registerUser(String email, String password, String fullName) async {}

  void checkAuthStatus() async {
    final token = await keyValueStorageService.getKeyValue<String>('token');
    if (token == null) return logout();

    try {
      final user = await authRepository.checkAuthStatus(token);
      _setLoggedUser(user);
    } catch (e) {
      logout();
    }
  }

  _setLoggedUser(User user) async {
    await keyValueStorageService.setKeyValue('token', user.token);

    state = state.copyWith(
      user: user,
      authStatus: AuthStatus.authenticated,
      errorMessage: '',
    );
  }

  Future<void> logout([String? errorMessage]) async {
    await keyValueStorageService.removeKey('token');

    state = state.copyWith(
      authStatus: AuthStatus.unauthenticated,
      user: null,
      errorMessage: errorMessage,
    );
  }
}

enum AuthStatus { checking, authenticated, unauthenticated }

class AuthState {
  final AuthStatus authStatus;
  final User? user;
  final String errorMessage;

  AuthState({
    this.authStatus = AuthStatus.checking,
    this.user,
    this.errorMessage = '',
  });

  AuthState copyWith({
    AuthStatus? authStatus,
    User? user,
    String? errorMessage,
  }) =>
      AuthState(
        authStatus: authStatus ?? this.authStatus,
        user: user ?? this.user,
        errorMessage: errorMessage ?? this.errorMessage,
      );
}

**app_router_notifier.dart**

final goRouterNotifierProvider = Provider((ref) {
  final authNotifier = ref.read(authProvider.notifier);
  return GoRouterNotifier(authNotifier);
});

class GoRouterNotifier extends ChangeNotifier {
  final AuthNotifier _authNotifier;

  AuthStatus _authStatus = AuthStatus.checking;

  GoRouterNotifier(this._authNotifier) {
    _authNotifier.addListener((state) {
      authStatus = state.authStatus;
    });
  }

  AuthStatus get authStatus => _authStatus;

  set authStatus(AuthStatus value) {
    _authStatus = value;
    notifyListeners();
  }
}

Now this is my second project using annotations (Here is the problem)

**auth_provider.dart**

part 'auth_provider.g.dart';

@Riverpod(keepAlive: true)
class Auth extends _$Auth {
  final authRepository = AuthRepositoryImpl();
  final keyValueStorageService = KeyValueStorageServiceImpl();

  @override
  AuthState build() {
    checkAuthStatus();
    return AuthState();
  } 

  Future<void> loginUser(String email, String password) async {
    try {
      final user = await authRepository.login(email, password);

      _setLoggedUser(user);
    } on CustomError catch (e) {
      logout(e.message);
    } catch (e) {
      logout('Error no controlado');
    }
  }

  void registerUser(String email, String password, String username) async {}

  void checkAuthStatus() async {
    final token = await keyValueStorageService.getKeyValue<String>('token');
    if (token == null) return logout();

    try {
      final user = await authRepository.checkAuthStatus(token);
      _setLoggedUser(user);
    } catch (e) {
      logout();
    }
  }

  _setLoggedUser(User user) async {
    await keyValueStorageService.setKeyValue('token', user.token);

    state = state.copyWith(
      user: user,
      authStatus: AuthStatus.authenticated,
      errorMessage: '',
    );
  }

  Future<void> logout([String? errorMessage]) async {
    await keyValueStorageService.removeKey('token');

    state = state.copyWith(
      authStatus: AuthStatus.unauthenticated,
      user: null,
      errorMessage: errorMessage,
    );
  }
}

enum AuthStatus { checking, authenticated, unauthenticated }

class AuthState {
  final AuthStatus authStatus;
  final User? user;
  final String errorMessage;

  AuthState({
    this.authStatus = AuthStatus.checking,
    this.user,
    this.errorMessage = '',
  });

  AuthState copyWith({
    AuthStatus? authStatus,
    User? user,
    String? errorMessage,
  }) =>
      AuthState(
        authStatus: authStatus ?? this.authStatus,
        user: user ?? this.user,
        errorMessage: errorMessage ?? this.errorMessage,
      );
}

**app_router_notifier.dart**

part 'app_router_notifier.g.dart';

@Riverpod(keepAlive: true)
GoRouterNotifier goRouterNotifier(Ref ref) {
  final authNotifier = ref.read(authProvider.notifier);

  return GoRouterNotifier(authNotifier);
}

class GoRouterNotifier extends ChangeNotifier {
  final Auth _authNotifier;

  AuthStatus _authStatus = AuthStatus.checking;

  GoRouterNotifier(this._authNotifier) {
   _authNotifier.addListener((state) {
      authStatus = state.authStatus;
    });
  }

  AuthStatus get authStatus => _authStatus;

  set authStatus(AuthStatus value) {
    _authStatus = value;
    notifyListeners();
  }
}

So what is the problem?

As you can see I'm trying to do the exact same thing, but in app_router_notifier.dart in this line:

_authNotifier.addListener((state) {
      authStatus = state.authStatus;
    });

I'm getting this error: The method 'addListener' isn't defined for the type 'Auth'. Try correcting the name to the name of an existing method, or defining a method named 'addListener'.

I've tried different solutions even with AI and nothing. I think that there's something wrong in my auth_provider.dart but I'm not sure what it is. I believe that the .addListener is in the StateNotifier but it isn't in the NotifierProvider that generates Riverpod.

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'auth_provider.dart';

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$authHash() => r'baf96bdb5085978702d0e2b04c3a2e56893b8a41';

/// See also [Auth].
@ProviderFor(Auth)
final authProvider = NotifierProvider<Auth, AuthState>.internal(
  Auth.new,
  name: r'authProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$authHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$Auth = Notifier<AuthState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter

There are other "solutions" that didn't work for me:

_authNotifier.listen<AuthStatus>((state) {
      authStatus = state.authStatus;
    });
_authNotifier.ref.listen(authProvider.notifier, (_, currentState) {
      authStatus = currentState.state.authStatus;
    });
 _authNotifier.addListener((state) {
      final authState = _authNotifier.build();
      authStatus = authState.authStatus;
    });

A little bit of context of the application

I'm using Flutter, Dart, Riverpod and Go Router to do a login and signup in my backend (NestJS + GraphQL), in the following file I'm trying to redirect the user if doesn't have a valid token. Here is where I'm using the goRouterNotifier so it can listen when the state changes and redirect the user.

part 'app_router.g.dart';

@Riverpod(keepAlive: false)
GoRouter goRouter(GoRouterRef ref) {
  final goRouterNotifier = ref.read(goRouterNotifierProvider);

  return GoRouter(
    initialLocation: '/login',
    refreshListenable: goRouterNotifier,
    routes: [
      //* First Route
      GoRoute(
        path: '/checking',
        builder: (context, state) => const CheckAuthStatusScreen(),
      ),

      //* Shared Routes
      GoRoute(
          path: '/main/:page',
          builder: (context, state) {
            final pageIndex = state.pathParameters['page'] ?? '0';
            return MainScreen(pageIndex: int.parse(pageIndex));
          }),

      GoRoute(
        path: '/settings',
        builder: (context, state) => const SettingdScreen(),
      ),

      //* Auth Routes
      GoRoute(
        path: '/login',
        builder: (context, state) => const LoginScreen(),
      ),
      GoRoute(
        path: '/signup',
        builder: (context, state) => const SignUpScreen(),
      ),

      //* Home Routes
    ],
    redirect: (context, state) {
      // print(state.matchedLocation);
      final isGoingTo = state.matchedLocation;
      final authStatus = goRouterNotifier.authStatus;

      if (isGoingTo == '/checking' && authStatus == AuthStatus.checking) {
        return null;
      }

      if (authStatus == AuthStatus.unauthenticated) {
        if (isGoingTo == '/login' || isGoingTo == '/signup') return null;

        return '/login';
      }

      if (authStatus == AuthStatus.authenticated) {
        if (isGoingTo == '/login' ||
            isGoingTo == '/signup' ||
            isGoingTo == '/checking') return '/main/0';
      }

      return null;
    },
  );
}

Solution

  • In fact, StateNotifier properties such as .stream or .addListener are not present in Notifier. So you should listen to Notifier like this:

    _authNotifier.ref.listenSelf((AuthState? previous, AuthState next) {
          authStatus = next.authStatus;
        });
    

    or use the ref.listen method (I think this option is the most optimal in this situation):

    ref.listen(_authNotifier, (AuthState? previous, AuthState next) {
          authStatus = next.authStatus;
        });
    

    You can pass ref to it through the constructor of the GoRouterNotifier class.

    In any other cases where you don't have access to ref, get the current container from the context (ProviderScope.containerOf(context)) and use providerContainer.listen(provider).