Need to get state data from Authentication Cubit inside another Cubit. In essence, I need to read the state - flutter

Note: I've seen similar questions all over Stackoverflow, but none of them exactly answer this.
Context:
I have an authentication cubit that has a variety of user states:
/// User exists, includes user's access and refresh tokens.
class User extends AuthenticationState {
final Tokens? tokens;
final bool justRegistered;
final bool tokensAvailable;
User({
required this.tokens,
this.justRegistered = false,
this.tokensAvailable = true,
});
#override
List<Object?> get props => [tokens, justRegistered, tokensAvailable];
}
/// No authenticated user exists.
class NoUser extends AuthenticationState {}
/// User is currently being registered or signed in.
class UserLoading extends AuthenticationState {}
Problem:
I have a Posts Cubit that needs to manage API calls and pass in the current user's access token to them in order to work. However, this access token is only stored inside the Authentication Cubit's state (in the User state, inside the tokens variable).
Is there a (good practice) way to get this token from the Authentication Cubit's User state inside the Post Cubit so I can use it to send API requests that require tokens for authentication to return posts?
Thanks!

You can create a class something like API Provider and authenticateService, pass and use in your Cubit to call API :
For example:
Future<Response<dynamic>> _callAndRetryDelete(
String url, {
int validStatus,
int retries,
}) async =>
retry(
() async {
final Map<String, String> authHeaders = await _authenticationService.getAuthHeaders;
if (authHeaders == null) {
throw ApiError('Could not get authorization', endpoint: url, method: HTTPMethod.Delete);
}
_client.options = _options(success: validStatus, authHeaders: authHeaders);
try {
return await _client.delete(url);
} on DioError catch (error) {
final String message = _error(error);
throw ApiError(message, endpoint: url, method: HTTPMethod.Delete);
}
},
maxAttempts: retries,
);
AuthenticaionService.class
Future<void> _doLogin(Map<String, dynamic> params) async {
final Response<Map<String, dynamic>> response =
await _handler.post(_loginEndpoint, body: params,
validStatus: 200);
final Map<String, String> res =
response?.data?.map((String key, dynamic value) => MapEntry<String, String>(key, value.toString()));
final Authentication authentication = Authentication.parse(res);
await _parseAndStore(authentication);
return authentication;}
AuthCubit:
Future<void> signIn() async => cubitAction(
action: () async {
emit(AuthenticationLoading());
await _authenticationService.signIn(payload);
emit(AuthenticationDone);
}
);

Related

Why is this listener never called?

I'm trying to use Riverpod for my project, however I'm hitting some issues.
I am not sure that I'm using it very well so don't hesitate to tell me if you see anything wrong with it :)
First I have my authProvider:
final authRepoProvider = ChangeNotifierProvider.autoDispose((ref) {
return AuthRepository();
});
class AuthRepository extends ChangeNotifier {
String? token;
Future signIn(String username, String password) async {
// Do the API calls...
token = tokenReturnedByAPI;
notifyListeners();
}
}
Then I have a service, let's say it allows to fetch blog Articles, with a stream to get live update about those.
class ArticleService {
StreamController<Article> _streamCtrl;
String? _token;
API _api;
ArticleService(this._api) : _streamCtrl = StreamController<Article>() {
_api.onLiveUpdate((liveUpdate) {
_streamCtrl.add(liveUpdate);
});
}
Stream<Article> get liveUpdates => _streamCtrl.stream;
Future markArticleAsRead(String id) async {
await _api.markAsRead(_token, id);
}
}
For that article service I would like to keep the current token up to date, but I don't want to rebuild the entire service every time the token changes as there are listeners and streams being used.
For that I would prefer to listen to the changes and update it myself, like such:
final articleServiceProvider = Provider.autoDispose((ref) {
final service = ArticleService(
ref.read(apiProvider),
);
ref.listen<AuthRepository>(authRepositoryProvider, (previous, next) {
service._token = next.token;
}, fireImmediately: true);
return service;
});
That piece of code seems correct to me, however when I authenticate (authRepository.token is definitely set) and then try to invoke the markArticlesAsRead method I end up with an empty token.
The ref.listen is never called, even tho AuthRepository called notifyListeners().
I have a feeling that I'm using all that in a wrong way, but I can't really pinpoint what or where.
Try ref.watch
final articleServiceProvider = Provider.autoDispose((ref) {
final service = ArticleService(
ref.read(apiProvider),
);
final repo = ref.watch<AuthRepository>(authRepositoryProvider);
service._token = repo.token;
return service;
});

Read a provider inside a FutureProvider

When we need to read (not watch) a provider inside another one the documentation is clear:
"DON'T CALL READ INSIDE THE BODY OF A PROVIDER"
final myProvider = Provider((ref) {
// Bad practice to call `read` here
final value = ref.read(anotherProvider);
});
And it suggest to pass to the value exposed the Reader function: https://riverpod.dev/docs/concepts/combining_providers#can-i-read-a-provider-without-listening-to-it
final userTokenProvider = StateProvider<String>((ref) => null);
final repositoryProvider = Provider((ref) => Repository(ref.read));
class Repository {
Repository(this.read);
/// The `ref.read` function
final Reader read;
Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);
final response = await dio.get('/path', queryParameters: {
'token': token,
});
return Catalog.fromJson(response.data);
}
}
And that's ok, but what is the best practice when I need to read a provider inside a FutureProvider?
I find myself in this situation many time because I expose the api as providers and inside the FutureProvider I call watch to get the api I need.
But I noticed that, because I'm watching the Api provider inside the userProvider, this won't gets disposed after been used.
Here's an example of what I'm trying to say:
API CLASS
final userApiProvider = Provider((ref) => UserApi(ref.read));
class UserApi {
final Dio _dio;
const UserApi(Reader read):
_dio = read(dioProvider);
Future<Response> getUser(String id, { CancelToken? cancelToken }) async{
final _url = '$URL_TO_API/$id';
return _dio.get(_url, cancelToken: cancelToken);
}
}
When using the API inside a FutureProvider
final userProvider = FutureProvider.autoDispose.family<User, int>((ref, userId) async {
final userApi = **ref.watch(userApi);**
final cancelToken = CancelToken();
ref.onDispose(() { cancelToken.cancel(); });
final user = await userApi.getUser(cancelToken: cancelToken);
return user;
});
The same logic applies.
By "Don't use read inside a provider", it isn't talking about the class Provider specifically, but any provider – so FutureProvider included.
In general, you should avoid using read as much as possible.

Updating State with Provider after API call

How do I update the state with Provider after I've called the API to update it to the BE? I pass the arguments from one screen to another, then after I edit the text, I trigger the API call to the BE and I get the return value of the response and I want to update the state with that response with Provider. How is that possible? Here is the code:
Here I call the API and pass the arguments I've edited in my text fields:
onPressed: () async {
final updatedUser = await await APICall.updateUser(
userID,
updateName,
updateEmail,
);
Provider.of<UserStore>(context, listen: false)
.updateUser(updatedUser);
Navigator.pop(context);
},
Here is the API call where I return the response of the updated User:
Future<User> updateUser(String userID, String name, String email) async {
final response =
await APICalls.apiRequest(Method.PATCH, '/users', this._jsonWebToken,
body: jsonEncode({
"id": userID,
"name": name,
"email": email,
}));
Map<String, dynamic> jsonDecodedResponse = jsonDecode(response.body);
return User(
id: jsonDecodedResponse['data']['id'],
name: jsonDecodedResponse['data']['name'],
email: jsonDecodedResponse['data']['email'],
);
}
Now I wanted to pass that response I've got from the API call to pass it to the providers state:
deleteUser(User list) {
_userList.remove(list);
notifyListeners();
}
addUser(User list) {
_userList.add(list);
notifyListeners();
}
updateUser(User ){//I'm not sure how do define the updateUser method...
notifyListeners();
}
The update works on the BE side, and on the FE only when I refresh the widget, not immediately after the response is returned, which is the way I want it to work.
class UserStore extends ChangeNotifier {
List<User> clientList = [];
void deleteUser(User list) {
clientList.remove(list);
notifyListeners();
}
void addClient(User list) {
clientList.add(list);
notifyListeners();
}
void updateUser(User user){
clientList[clientList.indexWhere((element) => element.id == user.id)] = user;
notifyListeners();
}
}
What you have to do now is to listen to this provider on your widget. When the user will be updated, the changes will be applied to any widget listening to the clientList.
Note I've changed the clientList variable to public, so it can be listened by any widget outside.

Flutter api login using riverpod

I'm trying to use riverpod for login with a laravel backend. Right now I'm just returning true or false from the repository. I've set a form that accepts email and password. The isLoading variable is just to show a circle indicator. I've run the code and it works but not sure if I'm using riverpod correctly. Is there a better way to do it ?
auth_provider.dart
class Auth{
final bool isLogin;
Auth(this.isLogin);
}
class AuthNotifier extends StateNotifier<Auth>{
AuthNotifier() : super(Auth(false));
void isLogin(bool data){
state = new Auth(data);
}
}
final authProvider = StateNotifierProvider((ref) => new AuthNotifier());
auth_repository.dart
class AuthRepository{
static String url = "http://10.0.2.2:8000/api/";
final Dio _dio = Dio();
Future<bool> login(data) async {
try {
Response response = await _dio.post(url+'sanctum/token',data:json.encode(data));
return true;
} catch (error) {
return false;
}
}
}
login_screen.dart
void login() async{
if(formKey.currentState.validate()){
setState((){this.isLoading = true;});
var data = {
'email':this.email,
'password':this.password,
'device_name':'mobile_phone'
};
var result = await AuthRepository().login(data);
if(result){
context.read(authProvider).isLogin(true);
setState((){this.isLoading = false;});
}else
setState((){this.isLoading = false;});
}
}
Since I'm not coming from mobile background and just recently use flutter+riverpod in my recent project, I cannot say this is the best practice. But there are some points I'd like to note:
Use interface such IAuthRepository for repository. Riverpod can act as a dependency injection.
final authRepository = Provider<IAuthRepository>((ref) => AuthRepository());
Build data to send in repository. You should separate presentation, business logic, and explicit implementation for external resource if possible.
Future<bool> login(String email, String password) async {
try {
var data = {
'email': email,
'password': password,
'device_name':'mobile_phone'
};
Response response = await _dio.post(url+'sanctum/token',data:json.encode(data));
return true;
} catch (error) {
return false;
}
}
Do not call repository directly from presentation/screen. You can use the provider for your logic, which call the repository
class AuthNotifier extends StateNotifier<Auth>{
final ProviderReference ref;
IAuthRepository _authRepository;
AuthNotifier(this.ref) : super(Auth(false)) {
_authRepository = ref.watch(authRepository);
}
Future<void> login(String email, String password) async {
final loginResult = await_authRepository.login(email, password);
state = Auth(loginResult);
}
}
final authProvider = StateNotifierProvider((ref) => new AuthNotifier(ref));
On screen, you can call provider's login method
login() {
context.read(authProvider).login(this.email, this.password);
}
Use Consumer or ConsumerWidget to watch the state and decide what to build.
It also helps that instead of Auth with isLogin for the state, you can create some other state. At the very least, I usually create an abstract BaseAuthState, which derives to AuthInitialState, AuthLoadingState, AuthLoginState, AuthErrorState, etc.
class AuthNotifier extends StateNotifier<BaseAuthState>{
...
AuthNotifier(this.ref) : super(AuthInitialState()) { ... }
...
}
Consumer(builder: (context, watch, child) {
final state = watch(authProvider.state);
if (state is AuthLoginState) ...
else if (state is AuthLoadingState) ...
...
})
Instead of using a bool, I like to use enums or class for auth state
enum AuthState { initialize, authenticated, unauthenticated }
and for login state
enum LoginStatus { initialize, loading, success, failed }

How do I print a value from an instance of 'User' in a flutter?

class User {
String token;
User({this.token});
}
class AuthService {
final String url = 'https://reqres.in/api/login';
final controller = StreamController<User>();
Future<User> signIn(String email, String password) async {
final response =
await post(url, body: {'email': email, 'password': password});
final data = jsonDecode(response.body);
final user = _userFromDatabaseUser(data);
// print(user.token);
controller.add(user);
return user;
}
//create user obj based on the database user
User _userFromDatabaseUser(Map user) {
return user != null ? User(token: user['token']) : null;
}
//user stream for provider
Stream<User> get user {
return controller.stream;
}
}
//in Sign in page
onPressed: () async {
if (_formKey.currentState.validate()) {
dynamic result = await _auth.signIn(email, password);
print(result); // Instance of 'User'
}
}
I am new to flutter and want to make an app that only authenticated users. I'm trying to read user token data from a stream. then check that token is not null if I got token then goto home page otherwise it will show error how do I print or store token value?
You can do is when you get the user after the sign In:
User result = await _auth.signIn(email, password);
Then to see the data you can do is
print(result.token);
which will give you the token, and then you can use the shared prefrences to store your token and access it.
Check out the docs for the it: https://pub.dev/packages/shared_preferences
You can override Object.toString method.
you can add this method in your User class to print the token instead of Instance of 'User'.
#override
String toString() {
// TODO: change the below return to your desired string
return "token: $token";
}
You can print using
print(userModel.toString());