How to close a Function without disposing it. I needy this answer because when I log out, I need to close the functions in ChangeNotifier Class.
This Is my ChangeNotifier Class:
class ChatAndRequestProvider extends ChangeNotifier {
bool _areThereNewChatsAndRequests = false;
bool get areThereNewChatsAndRequests => _areThereNewChatsAndRequests;
set areThereNewChatsAndRequests(bool value) {
_areThereNewChatsAndRequests = value;
notifyListeners();
}
List _chatsList = [];
List get chatsList => _chatsList;
set chatsList(List list) {
_chatsList = list;
notifyListeners();
}
getChats() async {
var prefs = await SharedPreferences.getInstance();
print('The getChats Id is ${prefs.getString(kUserId)}');
FirebaseDatabase.instance
.reference()
.child('users')
.child(prefs.getString(kUserId))
.child('friendsArray')
.onValue
.listen((snapshot) {
Map list = snapshot.snapshot.value;
print('map is $list');
var newItems = [];
if (list != null) {
list.forEach((key, value) {
newItems.add(value);
});
chatsList = newItems;
var globalArray = [];
for (var item in newItems) {
if (item[kLastTimestamp] != item[kLastTimestampSeen]) {
areThereNewChatsAndRequests = true;
}
var status;
switch (item['friendsStatus']) {
case 'friends':
status = RequestStatus.alreadyAFriend;
break;
case 'notFriends':
status = RequestStatus.noRequest;
break;
case 'blocked':
status = RequestStatus.userThatBlockedMe;
break;
case 'unblocked':
status = RequestStatus.noRequest;
break;
}
globalArray.add({kUserId: item[kUserId], kTypeOfRequest: status});
}
valuesList = globalArray;
} else {
deleteFromList(null, RequestStatus.alreadyAFriend);
chatsList = [
{kUserId: 'null'}
];
}
});
}
So for example when I log In as user1 and I call this function in the LoadingScreen() I get all of users that are my friends, and I can go to the chats screen List and chat with my friends. Up to this point there is no issue. But when I log out and when I log in with another account lets say user2 and I call this function again, then I get error and two responses because I am calling this function twice. I am not using Auth Packet, I have my own database on MongoDB where I store user Info, but requests and chats are stored on RealTime Database.
So my Question is:
When user1 logs out of my app, I can not call dispose() on provider because if he wants to log in again to another account, he will get an error because Provider was disposed, so how can I stop listening to my database when user logs out and call this function again. Thank You very Much!!
I´m not sure if this works because I don't fully understand the flow of your app but you say that
I can not call dispose() on provider because if he wants to log in
again to another account,
when the users logs out shouldn't the app return to the first screen disposing the provider? (unless you create it in the MaterialApp, I'm not sure about that either). You could save the instance of the Firebase listener and then close it when you log out/ dispose the provider
var _myListener;
getChats() async {
var prefs = await SharedPreferences.getInstance();
print('The getChats Id is ${prefs.getString(kUserId)}');
_myListener = FirebaseDatabase.instance
.reference()
.child('users')
.child(prefs.getString(kUserId))
.child('friendsArray')
.onValue
.listen((snapshot) ...
....
/// The rest of your code
}
void closeListener(){ //call it when the user logs out
_myListener?.close();
}
#override
void dispose(){
closeListener(); // or call it in the dispose if you want
//to dispose and create a new provider when the user logs out/ sign in
super.dispose();
}
Related
I am doing an app in flutter and I am working on the authentication part. I want to know how I can keep my user logged in after I reload the app. Now the thing is that my app has 2 kinds of users (Client and Driver). So each has its own space, like sign in and sign up and main (after logging in).
This is the code that I used for logging.
class Initializer extends StatefulWidget {
// Access to this Screen
static String id = 'initializer';
#override
_InitializerState createState() => _InitializerState();
}
class _InitializerState extends State<Initializer> {
// Firebase Stuff
final _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
User _user;
// To Check if There's a Driver
bool isDriver = true;
void getCurrentUser() async {
try {
final getCurrentUser = _auth.currentUser;
if (getCurrentUser != null) {
getUserKind();
_user = getCurrentUser;
}
} catch (e) {
print(e);
}
}
getUserKind() async {
try {
// To fetch Database for Driver
final QuerySnapshot checkOfDriver =
await _firestore.collection('driver').where('uid', isEqualTo: _user.uid).get().catchError((error) {
print(error);
});
if (checkOfDriver.docs.isEmpty)
setState(() {
isDriver = false;
});
else
setState(() {
isDriver = true;
});
} catch (e) {
print(e);
return null;
}
}
#override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
#override
void initState() {
super.initState();
getCurrentUser();
}
#override
Widget build(BuildContext context) {
getCurrentUser();
SizeConfig().init(context);
return _user == null
? WelcomeScreen()
: isDriver
? DriverMain()
: ClientMain();
}
}
It's actually working but not properly, because when I reload the app while I'm logging in as a Client, the app shows me DriverMain at the beginning for one second then it switches to the right side which is ClientMain and that causes me some errors sometimes, and it's not an efficient work anyway.
So, what I should add to the code or ...
Firebase already persists the users credentials, and restores them automatically when the app restarts.
But this is an asynchronous process, as it requires a call to the server. By the time your getCurrentUser = _auth.currentUser code runs, that asynchronous process hasn't finished yet, so you get null.
To properly respond to the auth state being restored (and other changes), you'll want to use an auth state change listener as shown in the documentation on authentication state:
FirebaseAuth.instance
.authStateChanges()
.listen((User? user) {
if (user == null) {
print('User is currently signed out!');
} else {
print('User is signed in!');
}
});
If you want to use this in your UI, you'll typically wrap it in a StreamBuilder instead of calling listen yourself.
I'm getting this error in my StateNotifiers when one hits the back button on their webpage. I've isolated it to happening where the longRunningAPI request is below.
Exception has occurred.
"Error: Bad state: Tried to use RunListNotifier after `dispose` was called.
and I have code like this.
final runListController = StateNotifierProvider.autoDispose
.family<RunListNotifier, AsyncValue<List<Run>>, RunListParameter>(
(ref, param) {
return RunListNotifier(read: ref.read, param: param);
});
class RunListNotifier extends StateNotifier<AsyncValue<List<Run>>> {
RunListNotifier({required this.read, required this.param})
: super(AsyncLoading()) {
fetchViaAPI(param);
}
final Reader read;
final RunListParameter param;
void fetchViaAPI(RunListParameter param) async {
state = AsyncLoading();
try {
List<Run> stuff = await read(apiProvider).longRunningAPI(param: param);
state = AsyncData(stuff);
} catch (e) {
state = AsyncError(e);
}
}
}
is it safe to simply do something like this in the catch?
} catch (e) {
if (e.runtimeType.toString() == 'StateError') {
// ignore the error
} else {
state = AsyncError(e);
}
}
I believe you could solve this problem by checking mounted before setting the state after your API call like so:
List<Run> stuff = await read(apiProvider).longRunningAPI(param: param);
if (!mounted) return;
state = AsyncData(stuff);
This simply checks if dispose was called and if so, don't attempt to modify the state.
Another resource that could be useful is adding a cancelToken to your API call and canceling if the provider is disposed.
final longRunningApi = FutureProvider.autoDispose.family<List<Run>, RunListParameter>((ref, param) async {
final cancelToken = CancelToken();
ref.onDispose(cancelToken.cancel);
final api = await ref.watch(apiProvider);
final res = await api.longRunningApi(param, cancelToken);
ref.maintainState = true;
return res;
});
Then you'd have to add the cancelToken to your actual request. A great example of this in the marvel example project by the author of Riverpod can be found here.
I created a separate calss page to working with shared preferences from all the different application pages. Save or edit data. I can save String data with ease, but I am facing a problem saving data of type bool. I try to save data of type bool to store the status of the user logged in or not. I searched for solutions for a long time, but couldn't find.
full code:
import 'package:shared_preferences/shared_preferences.dart';
class MyPreferences {
static const ID = "id";
static const STATE = "state";
static final MyPreferences instance = MyPreferences._internal();
static SharedPreferences _sharedPreferences;
String id = "";
String state = "";
MyPreferences._internal() {}
factory MyPreferences() => instance;
Future<SharedPreferences> get preferences async {
if (_sharedPreferences != null) {
return _sharedPreferences;
} else {
_sharedPreferences = await SharedPreferences.getInstance();
state = _sharedPreferences.getString(STATE);
id = _sharedPreferences.getString(ID);
return _sharedPreferences;
}
}
Future<bool> commit() async {
await _sharedPreferences.setString(STATE, state);
await _sharedPreferences.setString(ID, id);
}
Future<MyPreferences> init() async {
_sharedPreferences = await preferences;
return this;
}
}
Can somebody help me to make bool data.
thank you
Just add a couple methods to your class.
void updateLoggedIn(bool value) {
_sharedPreferences.setBool('logged_in', value);
}
bool isLoggedIn() => _sharedPreferences.getBool('logged_in') ?? false;
Then on login just run
MyPreferences.instance.updateLoggedIn(true)
And the same thing passing in false on logout.
Then whenever you want to check logged in status just run
if(MyPreferences.instance.isLoggedIn()) {
// whatever needs to happen
}
I have a list of items and a stream within a class. The stream triggers a future where then notifylisteners is called to update the list of items. It works, but it only shows updates within the stream. How do I notifylistners outside the stream as well?
Where, if I were to call Provider.of(context).items it won't return as empty.
Here is the following code structure.
class Mans with ChangeNotifier {
List<Man> _items = [];
List<Man> get items {
return [..._items];
}
Stream<List<Man>> stream;
bool hasMore;
bool _isLoading;
List<Man> _data;
StreamController<List<Man>> _controller;
Mans({page = 1}) {
_data = List<Man>();
_controller = StreamController<List<Man>>.broadcast();
_isLoading = false;
// Test if list prints #1
items.forEach((list) {
print("nono: ${list.id}");
});
stream = _controller.stream.map((List<Man> mansData) {
// Test if list prints #2
items.forEach((list) {
print("nono: ${list.id}");
});
return mansData;
});
// Test if list prints #3
items.forEach((list) {
print("nono2: ${list.id}");
});
hasMore = true;
refresh();
}
Future<void> refresh() {
return loadMore(
page: 1,
clearCachedData: true,
);
}
Future<void> loadMore(
{bool clearCachedData = false,
page = 1}) async {
if (clearCachedData) {
_data = List<Man>();
hasMore = true;
}
if (_isLoading || !hasMore) {
return Future.value();
}
_isLoading = true;
return await fetchAndSetMans(page)
.then((mansData) {
_isLoading = false;
_data.addAll(mansData);
hasMore = (mansData.isNotEmpty);
_controller.add(_data);
});
}
Future<List<Man>> fetchAndSetMans(page) async {
var cookie = '';
try {
print('Called_API_Mans');
var response = await SiteApi(serverConfig["url"]).getAsync("api_link?view_id=$page");
List<Man> list = [];
for (var item in response) {
//This just adds an instance of Man to the list from a Model not added to this Stack Question. It works.
list.add(Man.fromJson(item));
}
_items = list;
notifyListeners();
return _items;
} catch (error) {
return [];
}
}
As you can see, I placed three different instances where I can print the items after notifyListeners() is called in the Future 'fetchAndSetMans'.
Unfortunately, only in the one where the comment says "Test if list prints #2" does it show that the list has been updated. Basically, within the stream data.
#1 and #3 are empty.
So, anything outside of the stream, notifyListeners() doesn't update the items list.
I wish to know how I can update the value outside the stream when the future is called.
So, if I call a Provider.... like, Provider.of(context).items... I can actually get results.
Thanks, I'd appreciate any help.
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 }