I am trying to persist the user authentication state of an already working auth system using flutter_bloc and Nodejs
, by storing my successfully retrieved and generated JWTs. I have chosen flutter_secure_storage (FSS), for brevity
as my key-value cache system.
Whenever I try to read or write data with FSS
, I keep running into these kind of errors:
Unhandled Exception: NoSuchMethodError: The method 'write/read/delete' was called on null.
In the case of writing my JWTs to FSS, here's an example snippet:
@override
Future<UserModel> loginUser(Map<String, dynamic> body) async {
final userModel = await dataSource.loginUser(body);
var userId = userModel.id;
Future.delayed(Duration(seconds: 2), () async {
await storage.write(key: APIConstants.USER_ID_KEY, value: userId);
});
return userModel;
}
Have even tried delaying the write
operation to see if the value will save, it only logs me in cos of proper credentials and then throws an error, not crashing though, that's great.
Here's the data source
class snippet for login user:
abstract class UserRemoteDataSource {
Future<UserModel> loginUser(Map<String, dynamic> body);
Future<UserModel> registerUser(Map<String, dynamic> body);
// Future<User> sendOTP();
// Future<User> verifyOTP();
Future<UserModel> getCurrentUser(String userId);
Future<void> logOut();
}
class UserRemoteDataSourceImpl implements UserRemoteDataSource {
final APIClient client;
var storage = FlutterSecureStorage();
UserRepository repository;
UserRemoteDataSourceImpl({@required this.client});
@override
Future<UserModel> loginUser(Map<String, dynamic> body) async {
final response = await client.postAuthData('login', body);
final userResponseModel = UserResponseModel.fromJSON(response);
final accessToken = userResponseModel.accessToken;
final refreshToken = userResponseModel.refreshToken;
storage = FlutterSecureStorage();
await storage.write(key: APIConstants.ACCESS_TOKEN_KEY, value: accessToken)
.then((value) => print('AccessToken Written to FSS')); // this callback from future logs, so I know that the tokens have been saved
await storage.write(key: APIConstants.REFRESH_TOKEN_KEY, value: refreshToken)
.then((value) => print('RefreshToken Written to FSS'));
return userResponseModel.user;
}
@override
Future<UserModel> registerUser(Map<String, dynamic> body) async {
final response = await client.postAuthData('register', body);
final userResponseModel = UserResponseModel.fromJSON(response);
final accessToken = userResponseModel.accessToken;
final refreshToken = userResponseModel.refreshToken;
storage = FlutterSecureStorage();
await storage.write(key: APIConstants.ACCESS_TOKEN_KEY, value: accessToken)
.then((value) => print('AccessToken Written to FSS'));
await storage.write(key: APIConstants.REFRESH_TOKEN_KEY, value: refreshToken)
.then((value) => print('AccessToken Written to FSS'));
return userResponseModel.user;
}
@override
Future<UserModel> getCurrentUser(String id) {
return null; // cos of write and read operation for userid, had to just return null for now
}
@override
Future<void> logOut() async {
await client.postAuthData('logout', null);
}
}
Now, when I compile my code, cos of the Delayed Future in Repository Snippet (First One Above)
, I get this error (showing full logs):
I/flutter ( 5048): Login state is LoginStateInitial
I/flutter ( 5048): LoginState Listened is: LoginStateLoading
I/flutter ( 5048): LoginState Building is: LoginStateLoading
I/flutter ( 5048): {success: true, message: Logged in Successfully, user: {_id: 60435f3a0b5da10015bb87b3, username: abc123, email: abc123@gmail.com, password: $2b$10$wgdBFyvWUI4e0bB3KNRBl.WLLVwu2FVZQN9BtSSdxbSzfcwdQyH2K, phone: 89066060484, __v: 0}, accessToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MTgxMjU2NTEsImV4cCI6MTYyMDcxNzY1MSwiYXVkIjoiNjA0MzVmM2EwYjVkYTEwMDE1YmI4N2IzIiwiaXNzIjoicGlja3VycGFnZS5jb20ifQ.uH3D_v6mYwIOnBgRfoQQYzrN8asPt_Rhfei9wxmAM6A, refreshToken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MTgxMjU2NTEsImV4cCI6MTY0OTY4MzI1MSwiYXVkIjoiNjA0MzVmM2EwYjVkYTEwMDE1YmI4N2IzIiwiaXNzIjoicGlja3VycGFnZS5jb20ifQ.dIgOHlsXdBXUtrLNqiF3mKhmKZU0HJe-IwMDyg23OO0}
D/FlutterSecureStoragePl( 5048): Initializing StorageCipher
I/fluttersecurestorage( 5048): Creating keys!
I/fluttersecurestorage( 5048): Initializing
I/fluttersecurestorage( 5048): Generating key pair
E/KeyStore( 5048): generateKeyInternal failed on request -68
D/FlutterSecureStoragePl( 5048): StorageCipher initialization complete
D/FlutterSecureStoragePl( 5048): StorageCipher already initialized
I/flutter ( 5048): LoginState Listened is: LoginStateSuccess
W/utter_bloc_aut( 5048): Accessing hidden method Lsun/misc/Unsafe;-
>compareAndSwapObject(Ljava/lang/Object;JLjava/lang/Object;Ljava/lang/Object;)Z (greylist, linking, allowed)
E/flutter ( 5048): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception:
// starts from here // NoSuchMethodError: The method 'write' was called on null.
E/flutter ( 5048): Receiver: null
E/flutter ( 5048): Tried calling: write(key: "userId", value: "60435f3a0b5da10015bb87b3") // in repository file
E/flutter ( 5048): #0 Object.noSuchMethod (dart:core-patch/object_patch.dart:54:5)
E/flutter ( 5048): #1 UserRepositoryImpl.loginUser.<anonymous closure> (package:flutter_bloc_auth/data/repositories/user_repository_impl.dart:22:21)
E/flutter ( 5048): #2 UserRepositoryImpl.loginUser.<anonymous closure> (package:flutter_bloc_auth/data/repositories/user_repository_impl.dart:21:42)
E/flutter ( 5048): #3 new Future.delayed.<anonymous closure> (dart:async/future.dart:315:39)
Is this a prime example of using null safety
and how? I had previously tried migrating the entire project to null safety but there were cyclic dependencies on the packages, so I continued without null safety.
Is this a case of using FutureBuilder
to wait for the value of the strings (tokens and userid). Had tried calling .then
but my response body (JSON String) is not a Future, obviously.
What's the cause of this error as we can see it later held the value of userId
in E/flutter ( 5048): Tried calling: write(key: "userId", value: "60435f3a0b5da10015bb87b3")
but initially it didn't, as per my understanding.
I've run into this error severally and still haven't found a way to deal with it. Any help will be appreciated.
--------EDIT (SIMILAR ISSUE)------
I've gotten the tokens and userid to save when login method is called. However, when I close the app and re-open it, it throws an error for reading the access token key (which was saved previously), like below log:
I/flutter (27570): AuthenticationStateInitial
I/flutter (27570): LoginState Building is: LoginStateInitial
I/flutter (27570): NoSuchMethodError: The method 'read' was called on null.
I/flutter (27570): Receiver: null
I/flutter (27570): Tried calling: read(key: "access_token")
I/flutter (27570): AuthenticationStateFailure
I want the AuthenticationBloc to read the state once it finds the token as Authenticated not Failure, each time a previously logged in or just signed up user opens the app. What's the best strategy for that?
Here's my AuthenticationBloc
snippet. Login & Register Blocs Snippets Skipped Due to Brevity and Similarity of Functionality.
class AuthenticationBloc
extends Bloc<AuthenticationEvent, AuthenticationState> {
final UserRepository _repository;
AuthenticationBloc(UserRepository repository)
: assert(repository != null), _repository = repository,
super(AuthenticationStateInitial());
@override
Stream<AuthenticationState> mapEventToState(
AuthenticationEvent event) async* {
if (event is AppStarted) {
yield* _mapAppStartedToState(event);
}
if(event is UserSignedUp){
yield* _mapUserSignedUpToState(event);
}
if(event is UserLoggedIn){
yield* _mapUserLoggedInToState(event);
}
if(event is UserLoggedOut){
await _repository.deleteTokens(event.accessToken, event.refreshToken);
yield* _mapUserLoggedOutToState(event);
}
}
Stream<AuthenticationState> _mapAppStartedToState(AppStarted event) async*
{
yield AuthenticationStateLoading();
try{
// if(event?.accessToken == null || event.refreshToken == null) {
// yield AuthenticationStateUnAuthenticated();
// } else {
// print('accessToken is\t${event?.accessToken}');
// print('refreshToken is\t${event?.refreshToken}');
// try {
// final currentUser = await _repository.getCurrentUser();
// yield AuthenticationStateAuthenticated(user: currentUser, );
// } catch(error2) {
// print(error2);
// }
// }
final hasTokens = await _repository.checkHasTokens(event.accessToken, event.refreshToken); //simple read operation using the Keys used to write tokens values
print('hasTokens $hasTokens');
final currentUser = await _repository.getCurrentUser();
// if(hasTokens && currentUser != null){
if(hasTokens){
yield AuthenticationStateAuthenticated(user: currentUser, );
} else {
yield AuthenticationStateUnAuthenticated();
}
} catch(err){
print(err);
// yield AuthenticationStateFailure(errorMessage: err.message ?? 'Error Completing Your Request, Try Again!');
yield AuthenticationStateFailure(errorMessage: 'Error Completing Your Request, Try Again!');
}
}
Stream<AuthenticationState> _mapUserSignedUpToState(UserSignedUp event) async* {
yield AuthenticationStateAuthenticated(user: event.user);
}
Stream<AuthenticationState> _mapUserLoggedInToState(UserLoggedIn event) async* {
yield AuthenticationStateAuthenticated(user: event.user);
}
Stream<AuthenticationState> _mapUserLoggedOutToState(UserLoggedOut event) async* {
await _repository.logOut();
yield AuthenticationStateUnAuthenticated();
}
}
and UserRepositoryImpl
Snippet, as Suggested:
class UserRepositoryImpl extends UserRepository {
final UserRemoteDataSource dataSource;
UserRepositoryImpl({@required this.dataSource});
var storage;
@override
Future<UserModel> loginUser(Map<String, dynamic> body) async {
final userModel = await dataSource.loginUser(body);
// var userId = userModel.id;
// Future.delayed(Duration(seconds: 2), () async {
// await storage.write(key: APIConstants.USER_ID_KEY, value: userId);
// });
return userModel;
}
@override
Future<UserModel> registerUser(Map<String, dynamic> body) async {
final userModel = await dataSource.registerUser(body);
// await storage.write(key: APIConstants.USER_ID_KEY, value: userModel.id);
return userModel;
}
@override
Future<UserModel> getCurrentUser() async {
// final userId = await storage.read(key: APIConstants.USER_ID_KEY);
// final user = await dataSource.getCurrentUser(userId);
// return user;
return null;
}
@override
Future<void> logOut() async {
storage = FlutterSecureStorage();
await storage.delete(key: APIConstants.ACCESS_TOKEN_KEY);
await storage.write(key: APIConstants.REFRESH_TOKEN_KEY);
await dataSource.logOut();
}
@override
Future<void> saveTokens(String accessToken, String refreshToken) async {
final storage = FlutterSecureStorage();
print('st() accesstokenval\t:$accessToken');
await storage.write(key: APIConstants.ACCESS_TOKEN_KEY, value: accessToken);
await storage.write(key: APIConstants.REFRESH_TOKEN_KEY, value: refreshToken);
}
@override
Future<void> deleteTokens(String accessToken, String refreshToken) async {
storage = FlutterSecureStorage();
await storage.delete(key: APIConstants.ACCESS_TOKEN_KEY);
await storage.write(key: APIConstants.REFRESH_TOKEN_KEY);
}
@override
Future<bool> checkHasTokens(String accessToken, String refreshToken) async{
storage = FlutterSecureStorage();
final hasAT = await storage.read(key: APIConstants.ACCESS_TOKEN_KEY);
final hasRT = await storage.read(key: APIConstants.REFRESH_TOKEN_KEY);
return hasRT == null || hasAT == null ? false : true;
}
}