How to navigate to another page after using a provider function in flutter? - flutter

I have created a function in my Login provider to verify OTP for my app like below.
Future<bool> verifyOtp(String otp) async {
final _loginData = await SharedPreferences.getInstance();
_isLoading = true;
notifyListeners();
_status = await AuthApi.verifyOtp(otp);
_isLoading = false;
_name = _loginData.getString('name');
notifyListeners();
return _status;
}
Now whenever I am trying to use this on my code like below,
final bool status = await Provider.of<LoginProvider>(context, listen: false).verifyOtp(verificationCode);
// ignore: avoid_print
print("STATUS ==== " + status.toString());
if (status) {
Navigator.of(context).pushReplacementNamed('/discover');
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Incorrect OTP!!!")));
}
It's giving me an exception like below -
Exception has occurred.
FlutterError (This widget has been unmounted, so the State no longer has a context (and should be considered defunct).
Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.)
Anyone please guide me, what is the actual way to navigate from a provider. I am very new in Provider. Thank you so much :)
----- Full Provider Code is Below -----
class LoginProvider with ChangeNotifier {
bool _status = false;
bool _isLoading = false;
bool _isOtpScreen = false;
String? _name;
bool get isLoading => _isLoading;
bool get isOtpScreen => _isOtpScreen;
String? get name => _name;
void sendOtp(String phone) async {
_isLoading = true;
notifyListeners();
_status = await AuthApi.sendOtp(phone);
_isLoading = false;
_isOtpScreen = true;
notifyListeners();
}
Future<bool> verifyOtp(String otp) async {
final _loginData = await SharedPreferences.getInstance();
_isLoading = true;
notifyListeners();
_status = await AuthApi.verifyOtp(otp);
_isLoading = false;
_name = _loginData.getString('name');
notifyListeners();
return _status;
}
}

Use a GlobalKey you can access from anywhere to navigate
Create the key
final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
Pass it to your App:
new MaterialApp(
title: 'MyApp',
onGenerateRoute: generateRoute,
navigatorKey: navigatorKey,
);
Use in your route:
print("STATUS ==== " + status.toString());
if (status) {
Navigator.of(navigatorKey.currentContext).pushReplacementNamed('/discover');
} else {
ScaffoldMessenger.of(navigatorKey.currentContext).showSnackBar(const SnackBar(content:
Text("Incorrect OTP!!!")));
}

final bool status = await Provider.of<LoginProvider>(context, listen: false).verifyOtp(verificationCode);
// ignore: avoid_print
print("STATUS ==== " + status.toString());
if (status) {
Navigator.of(context).pushReplacementNamed('/discover');
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Incorrect OTP!!!")));
}
change this code to the next one and i think it will work
but you must add this code in build function if it's stateless widget
final provider = Provider.of<LoginProvider>(context);
final status = await provider.verifyOtp(verificationCode);
// ignore: avoid_print
print("STATUS ==== " + status.toString());
if (status) {
Navigator.of(context).pushReplacementNamed('/discover');
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Incorrect OTP!!!")));
}
this will work fine with you i hope it help you

Related

Difference between Buildcontext and NavigatorKey.currentState.context

I'm currently using Provider as state management and also to keep all my function in it. At first i was using a callback method for me to to navigate thru screen when function in my Provider class succeed.
Future login(String email, String password, Function callback) async {
_isLoading = true;
notifyListeners();
bool isSuccess = false;
try {
final ApiResponse apiResponse = await authRepo!.login(email, password);
if (apiResponse.response != null && apiResponse.response!.statusCode == 200) {
isSuccess = true;
callback(isSuccess, apiResponse.response!.data[Constants.responseMsg]);
} else {
callback(isSuccess, apiResponse.error);
}
} catch (e) {
_isLoading = false;
print('login error: $e');
notifyListeners();
rethrow;
}
_isLoading = false;
notifyListeners();
}
but then i realized i could just pass the Buildcontext and navigating inside the function itself without using a callback method.
Future login(String email, String password, BuildContext context) async {
_isLoading = true;
notifyListeners();
try {
final ApiResponse apiResponse = await authRepo!.login(email, password);
if (apiResponse.response != null && apiResponse.response!.statusCode == 200) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) => DashboardScreen(),
settings: RouteSettings(name: '/Dashboard'),
),
);
} else {
GlobalFunction.showToast(apiResponse.error);
}
} catch (e) {
_isLoading = false;
print('login error: $e');
notifyListeners();
rethrow;
}
_isLoading = false;
notifyListeners();
}
and then i also realize i could use NavigatorKey.currentState!.context to navigate so i dont need the pass the Buildcontext.
Future login(String email, String password) async {
_isLoading = true;
notifyListeners();
try {
final ApiResponse apiResponse = await authRepo!.login(email, password);
if (apiResponse.response != null && apiResponse.response!.statusCode == 200) {
BuildContext _context = navigatorKey.currentState!.context;
Navigator.of(_context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) => DashboardScreen(),
settings: RouteSettings(name: '/Dashboard'),
),
);
} else {
GlobalFunction.showToast(apiResponse.error);
}
} catch (e) {
_isLoading = false;
print('login error: $e');
notifyListeners();
rethrow;
}
_isLoading = false;
notifyListeners();
}
i wonder which one is the better way?

State managment in flutter with consumer and scaffoldState

I'm using the Provider dependencie to manage states on my screen. Currently I have created a Loading Screen that works with Lottie animation. In my Sign In page, whenever there is an error with the log in, a Snackbar is shown to the user. Althought now, when I use the splash screen, the screen doesn't return and the snackBar isn't shown.
This is a piece of the login screen:
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: RaisedButton(
onPressed: userManager.loading
? null
: () {
if (formKey.currentState!
.validate()) {
userManager.signIn(
user: User(
email:
emailController.text,
password:
passController.text),
onFail: (e) {
scaffoldKey.currentState!
.showSnackBar(SnackBar(
content: Text(
'Falha ao entrar: $e'),
backgroundColor:
Colors.red,
));
},
onSucess: () {
debugPrint(
'Sucesso ao Logar');
Navigator.of(context).pop();
});
}
},
On the onFail I get this error, whenever I have a wrong password or other datas wrong:
Ocorreu uma exceção.
_CastError (Null check operator used on a null value)
This is how I'm changing between pages:
class LoginScreen extends StatelessWidget {
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final TextEditingController emailController = TextEditingController();
final TextEditingController passController = TextEditingController();
#override
Widget build(BuildContext context) {
return Consumer<UserManager>(builder: (_, userManager, child) {
if (userManager.loading) {
return SplashScreen();
} else {
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
UserManager:
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:loja_virtual_nnananene/helpers/firebase_errors.dart';
import 'package:loja_virtual_nnananene/models/user.dart';
class UserManager extends ChangeNotifier {
UserManager() {
_loadCurrentUser();
}
final FirebaseAuth auth = FirebaseAuth.instance;
User? user;
bool _loading = false;
bool get loading => _loading;
bool get isLoggedIn => user != null;
Future<void> signIn(
{required User user,
required Function onFail,
required Function onSucess}) async {
loading = true;
try {
final AuthResult result = await auth.signInWithEmailAndPassword(
email: user.email!, password: user.password!);
await _loadCurrentUser(firebaseUser: result.user);
onSucess();
} on PlatformException catch (e) {
onFail(getErrorString(e.code));
}
loading = false;
notifyListeners();
}
Future<void> signUp(
{required User user,
required Function onFail,
required Function onSucess}) async {
loading = true;
try {
final AuthResult result = await auth.createUserWithEmailAndPassword(
email: user.email!, password: user.password!);
user.id = result.user.uid;
this.user = user;
await user.saveData();
onSucess();
} on PlatformException catch (e) {
onFail(getErrorString(e.code));
}
loading = false;
notifyListeners();
}
void signOut() {
auth.signOut();
user = null;
notifyListeners();
}
set loading(bool value) {
_loading = value;
notifyListeners();
}
Future<void> _loadCurrentUser({FirebaseUser? firebaseUser}) async {
final FirebaseUser currentUser = firebaseUser ?? await auth.currentUser();
if (currentUser != null) {
final DocumentSnapshot docUser = await Firestore.instance
.collection('users')
.document(currentUser.uid)
.get();
user = User.fromDocument(docUser);
final docAdmin = await Firestore.instance
.collection('admins')
.document(user!.id!)
.get();
if (docAdmin.exists) {
user!.admin = true;
}
notifyListeners();
}
}
bool get adminEnabled => user != null && user!.admin;
}
Is there another way to set the splash screen thats easier?
While I wait for you to add the UserManager class implementation, I think there's a missing notifyListeners() in the signIn method.

Display Loading spinner waitint for request to complete while using provider package

I am using a provider package. I want to display a loading spinner while waiting for a request to complete. The pattern below is too verbose. Please help me make it less verbose. Here is my code
class APIService with ChangeNotifier {
// Check for working API backend
bool isWorking = false;
bool isLoading = false;
set _isLoading(bool value) {
isLoading = value; <--
notifyListeners();
}
Future<bool> selectAPI(String input) async {
_isLoading = true; <-- 1
final uri = Uri.tryParse('https://$input$url')!;
final response = await http.get(uri);
if (response.statusCode == 200) {
final body = jsonDecode(response.body) as Map<String, dynamic>;
bool isTrue = body['info']['title'] == 'SamFetch';
_isLoading = false; <-- 2
notifyListeners();
return isWorking = isTrue;
}
_isLoading = false; <-- 3
throw response;
}
}
Here is my UI code
IconButton(
icon: apiService.isLoading
? CircularProgressIndicator()
: Icon(Icons.done),
onPressed: () async {
await addAPI(apiService, cache);
}),
}
Below is addAPI() method
Future<void> addAPI(APIService apiService, Cache cache) async {
if (api != null) {
try {
await apiService.selectAPI(api!);
if (apiService.isWorking) {
await cache.saveAppName(api!);
}
} on SocketException catch (e) {
print(e);
} catch (e) {
await cache.clearCache();
}
}
}
Is setState the final solution?
You can use Future Builder and set your Future Function in future attribute. You can control the visible widget based on the status of your function. So you dont have to use isloading variable.

Correct way to call an api by provider in fflutter?

I have been trying to make a app in flutter where an api is called and data is updated in TextField
Used provider for state management, here is the code for it.
class ProfileProvider with ChangeNotifier {
var profileData;
String _url = "http://10.0.2.2:3000/api/v1/user/loggedin_user";
void getData() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
var token = prefs.getString('token');
var data = await http.get(
_url,
headers: {
"accept": "application/json",
"content-type": "application/json",
'Token': token,
},
);
var infoOfPerson = json.decode(data.body);
profileData = new ProfileObject(
name: infoOfPerson['name'],
mobile: infoOfPerson['mobile'],
email: infoOfPerson['email'],
role: infoOfPerson['role'],
);
notifyListeners();
}
ProfileObject get profileInfo {
return profileData;
}
}
I am getting the data fine, now i have to show it in the UI, but sometime data is populated, sometime its not. Can someone please point me the right direction why this is happening.
Here is the code for UI.
class Profile extends StatefulWidget {
#override
_ProfileState createState() => _ProfileState();
}
class _ProfileState extends State<Profile> {
final emailController = TextEditingController(text: '');
final nameController = TextEditingController(text: '');
final mobileController = TextEditingController(text: '');
var _isInit = true;
#override
void didChangeDependencies() {
if (_isInit) {
final profileData = Provider.of<ProfileProvider>(context);
profileData.getData();
if (profileData.profileInfo != null) {
emailController.text = profileData.profileInfo.name;
nameController.text = profileData.profileInfo.email;
mobileController.text = profileData.profileInfo.mobile;
}
_isInit = false;
super.didChangeDependencies();
}
}
#override
Widget build(BuildContext context) {
final profileData = Provider.of<ProfileProvider>(context);
return Scaffold(
drawer: NavigationDrawer(),
body: profileData.profileInfo == null
? Center(
child: CircularProgressIndicator(),
)
: Builder(
builder: (context) => SingleChildScrollView(
child: Padding(.....
Below the padding, there is normal TextField, can someone tell me why the data is being populated sometime and sometime its coming empty, even I wrapped it with CircularProgressIndicator() and a check the notifyListeners(); is not working there. The loader is not being shown and data is not being loaded.
Thanks
for StatelessWidget.
Inside the build method use:
Future.microtask(() async {
context.read<SomeProvider>().fetchSomething();
});
For StatefulWidgets if you want to call it once. Do this inside the initState() or didChangeDependencies (better if the latter). This will be called at the end of the frame which means after the build or rendering finishes..
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
context.read<SomeProvider>().fetchSomething();
});
}
EDIT: WidgetsBinding will also work on build. I forgot on why I used microtask lol
i've created a function which called nextTick, i call it in initState and it works for now, but want to see others method
void nextTick(Function callback, [int milliseconds = 0]) {
Future.delayed(Duration(milliseconds: 0)).then((_) {
callback();
});
}
then use it like below
#override
void initState() {
super.initState();
nextTick((){
ProfileProvider profileProvider = Provider.of<ProfileProvider>(context);
profileProvider.getProfile();
});
}
Edit: i store couple of variables to manage them on ui, like isLoading, hasError and errorMessage. Here is my provider class
class ProfileProvider extends ChangeNotifier {
bool _hasError = false;
bool _isLoading = true;
String _errorMsg = '';
Profile _profileResponse;
bool get hasError => _hasError;
bool get isLoading => _isLoading;
String get errorMsg => _errorMsg;
Profile get profileResponse => _profileResponse;
Future<void> getProfile() async {
this.setLoading = true;
this.setError = false;
this.setErrorMsg = '';
try {
await dio.post('$api/p/view', data: {}).then((res) {
print(res.data);
_profileResponse = Profile.fromJson(jsonDecode(res.data));
print(_profileResponse.data);
notifyListeners();
}).whenComplete(() {
this.setLoading = false;
});
} catch (e) {
this.setError = true;
this.setErrorMsg = '$e';
}
this.setLoading = false;
}
set setError(bool val) {
if (val != _hasError) {
_hasError = val;
notifyListeners();
}
}
set setErrorMsg(String val) {
if (val != null && val != '') {
_errorMsg = val;
notifyListeners();
}
}
set setLoading(bool val) {
_isLoading = val;
notifyListeners();
}
}

Show dialog using Scoped model

I have a basic login form, with my LoginModel.
But I do not understand how I can call to the function notifyListeners to display a dialog in my view.
The login widget:
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new ScopedModel<LoginModel>(
model: _loginModel,
child: Center(child: ScopedModelDescendant<LoginModel>(
builder: (context, child, model) {
if (model.status == Status.LOADING) {
return Loading();
}
else return showForm(context);
}))));
}
And the login model:
class LoginModel extends Model {
Status _status = Status.READY;
Status get status => _status;
void onLogin(String username, String password) async {
_status = Status.LOADING;
notifyListeners();
try {
await api.login();
_status = Status.SUCCESS;
notifyListeners();
} catch (response) {
_status = Status.ERROR;
notifyListeners();
}
}
I need to display a dialog when the status is Error
Finally I got this, just returning a Future in the method onLogin
Future<bool> onLogin(String username, String password) async {
_status = Status.LOADING;
notifyListeners();
try {
await api.login();
_status = Status.SUCCESS;
notifyListeners();
return true;
} catch (response) {
_status = Status.ERROR;
notifyListeners();
return false;
}
}
And in the widget:
onPressed: () async {
bool success = await _loginModel.onLogin(_usernameController.text, _passwordController.text);
if(success) {
Navigator.pop(context, true);
}
else{
_showDialogError();
}
}