ChangeNotifier shows error when navigate to next page - flutter

I'm new to flutter and building an app using ChangeNotifier and provider for an MVVM design pattern. When a login page loads I want to check it's already logged in or not. if it's already logged in, then navigate to the next page. But it show's following error.
My ChangeNotifier Binding code from splash screen
_moveToNextScreen() async {
await Navigator.pushReplacement(context,
MaterialPageRoute(
builder: (context) =>
ChangeNotifierProvider(
create: (_) => LoginViewModel(),
child: LoginScreen(),
),
));
}
Login Screen code
class _LoginScreenState extends State<LoginScreen> {
LoginViewModel _loginViewModel;
//controller for login
TextEditingController _usernameController = TextEditingController();
TextEditingController _passwordController = TextEditingController();
Size size; // calculate screen size
#override
void initState() {
_loginViewModel = Provider.of<LoginViewModel>(context, listen: false);
_loginViewModel.loggedInOrNot();
checkLoginStatus();
super.initState();
}
checkLoginStatus() async {
print('vcal - ${_loginViewModel.getIsLoggedIn}');
if (_loginViewModel.getIsLoggedIn == true) {
//_moveToNextScreen();
}
}
#override
Widget build(BuildContext context) {
_loginViewModel = Provider.of<LoginViewModel>(context);
size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: green,
//IP screen
// Asset Tracker- Text
body: _loginScreen(),
);
}
my LoginViewModel.dart
class LoginViewModel extends ChangeNotifier{
LoginRepository _loginRepository = LoginRepository();
bool _isLoading = false;
bool _isLoggedIn = false;
void loggedInOrNot() async {
_isLoggedIn = !_isLoggedIn;
//await _loginRepository.checkToken();
notifyListeners();
}
bool get getIsLoggedIn => _isLoggedIn;
bool get getIsLoading => _isLoading;
}
How can i resolve this issue?

You should access a provider like this
Provider.of<LoginViewModel>(context, listen: false).getIsLoggedIn;
and Wrap with Consumer widget with top of the ui class widget tree
Consumer is rebuilds your UI every state changes.
For more info about provider & consumer I recommend you to their documentation find it from here

It is maybe because when you go to the next page you create new ChangeNotifierProvider. You have to place it above these two Widgets, e.g. above the MaterialApp/CupertinoApp

Related

how to await for network connectivity status in flutter

I have used connectivity_plus and internet_connection_checker packages to check the internet connectivity.
The problem occured is , the app works perfectly fine as expected when the app start's with internet on state. But when the app is opened with internet off, the dialog isn't shown !!
I assume this is happening because the build method is called before the stream of internet is listened.
Code :
class _HomePageState extends State<HomePage> {
late StreamSubscription subscription;
bool isDeviceConnected = false;
bool isAlertSet = false;
#override
void initState() {
getConnectivity();
super.initState();
}
getConnectivity() {
subscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) async {
isDeviceConnected = await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
showDialogBox();
setState(() {
isAlertSet = true;
});
}
},
);
}
#override
void dispose() {
subscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
showDialogBox() => showDialog(/* no internet dialog */)
Extending the question: Is it assured that this works for all the pages ?
if yes, how ?
if not , how to overcome this?
First of all you need to listen for internet connectivity in your app first screen which is probably app.dart
GlobalKey<NavigatorState> navigatorKey = GlobalKey();
final noInternet = NoInternetDialog();
class TestApp extends StatefulWidget {
#override
State<TestApp> createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
#override
void initState() {
super.initState();
checkInternetConnectivity();
}
#override
Widget build(BuildContext context) {
return MaterialApp(...);
}
Future<void> checkInternetConnectivity() async {
Connectivity().onConnectivityChanged.getInternetStatus().listen((event)
{
if (event == InternetConnectionStatus.disconnected) {
if (!noInternet.isShowing) {
noInternet.showNoInternet();
}
}
});
}
}
Make the screen stateful in which you are calling MaterialApp and in initState of that class check for your internet connection, like above
You are saying how can I show dialog when internet connection changes for that you have to create a Generic class or extension which you can on connectivity change. You have to pass context to that dialogue using NavigatorKey
class NoInternetDialog {
bool _isShowing = false;
NoInternetDialog();
void dismiss() {
navigatorKey.currentState?.pop();
}
bool get isShowing => _isShowing;
set setIsShowing(bool value) {
_isShowing = value;
}
Future showNoInternet() {
return showDialog(
context: navigatorKey.currentState!.overlay!.context,
barrierDismissible: true,
barrierColor: Colors.white.withOpacity(0),
builder: (ctx) {
setIsShowing = true;
return AlertDialog(
elevation: 0,
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(3.0.h),
content: Container(...),
);
},
);
}
}
Use checkConnectivity to check current status. Only changes are exposed to the stream.
final connectivityResult = await Connectivity().checkConnectivity();

Go back to login when logged out from the drawer, no matter what

I need to redirect user to login page when he clicks on logout button from drawer (wherever he is). The problem is that when I click on the logout button, the screen remains the same.
According to this post: Flutter provider state management, logout concept
I have:
void main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
MyApp:
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
initPlatformState();
}
/// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
if (!mounted) return;
}
#override
Widget build(BuildContext context) {
return new MaterialApp(
initialRoute: '/',
navigatorKey: navigatorKey,
// ...
home: Consumer<Profile>(
builder: (context, profile, child){
return profile.isAuthenticated ? SplashScreen() : AuthScreen();
}
)
);
}
}
The part of the drawer where there is the logout button:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
}),
As I said, when I click on the logout button from the drawer, the user is correctly logged out, but the screen remains the same.
UPDATE
This is the profile class:
class Profile with ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated {
return this._isAuthenticated;
}
set isAuthenticated(bool newVal) {
this._isAuthenticated = newVal;
this.notifyListeners();
}
}
I think you are using provider class incorrectly.
use your profile class like this.
class Profile with ChangeNotifier {
bool _isAuthenticated = true;
bool get getIsAuthenticated => _isAuthenticated;
set setIsAuthenticated(bool isAuthenticated) {
_isAuthenticated = isAuthenticated;
notifyListeners();//you must call this method to inform lisners
}
}
in set method call notifyListners();
in your listTile
replace profile.isAuthenticated = false to profile.isAuthenticated = false;
Always use getters and setters for best practice.
I hope this is what you were looking for.
Add Navigator.of(context).pushReplacementNamed("/routeName") in LogOut onTap() Section.
For more information : https://api.flutter.dev/flutter/widgets/Navigator/pushReplacementNamed.html
Make sure to have logout route set in MyApp file, and i'd edit logout button file as such:
ListTile(
leading: Icon(Icons.logout),
title: Text(AppLocalizations.of(context)!.logout),
onTap: () async {
SharedPreferences preferences =
await SharedPreferences.getInstance();
await preferences.clear();
final Profile profile =
Provider.of<Profile>(context, listen: false);
profile.isAuthenticated = false;
// add login file route here using Navigator.pushReplacementNamed() ;
}),
Navigator push named -> logout route?

How can i show some loading screen or splash screen while flutter application loads up

I have been working on an app recently. I want to check if the user is logged in and is verified when my app loads up. So I created a Wrapper class to check if the user is logged in and is verified. Then accordingly I would show them either login screen or home screen.
I have assigned home : Wrapper(), in Main.dart .
After that I have wrapper class as
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
// checking if there is user and the user is verified
bool _isAuth() {
if (user != null && user.isVerified) {
return true;
}
return false;
}
return _isAuth() ? MainScreen() : Authenticate();
}
}
This works fine but the problem is it first flashes the login page and then takes me to the homepage if the user is logged in and is verified but it just works fine if the user is not logged in see gif image here
It probably shows the login page because of the way your logic is being handled. you should do this in initState instead of the build method. There are two ways to do this you can either use your wrapper as redirection class or use the build method like you're already doing to toggle the view.
First Method (uses redirection)
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
#override
void initState() {
super.initState();
final user = Provider.of<User>(context, listen: false);
var _isAuth = user != null && user.isVerified;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _isAuth ? MainScreen() : Authenticate()),
);
}
#override
Widget build(BuildContext context) {
return CircularProgressIndicator();
}
}
Second Method (uses build method):
class Wrapper extends StatefulWidget {
#override
_WrapperState createState() => _WrapperState();
}
class _WrapperState extends State<Wrapper> {
bool _isAuth = false;
bool _isLoading = true;
#override
void initState() {
super.initState();
final user = Provider.of<User>(context, listen: false);
setState(() {
_isAuth = user != null && user.isVerified;
_isLoading = false;
});
}
#override
Widget build(BuildContext context) {
return _isLoading
? CircularProgressIndicator()
: _isAuth
? MainScreen()
: Authenticate();
}
}

How to reference variable in method in FutureBuilder (builder:)?

I want to use the variable dbRef in inputData() in future Builder builder: you can see the variable in between asterisk .
void inputData() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
final **dbRef** = FirebaseDatabase.instance.reference().child("Add Job Details").child(uid).child("Favorites");
}
#override
Widget build(BuildContext context) {
return FutureBuilder (
future: **dbRef**.once(),
builder: (context, AsyncSnapshot<DataSnapshot> snapshot) {
if (snapshot.hasData) {
List<Map<dynamic, dynamic>> list = [];
for (String key in snapshot.data.value.keys) {
list.add(snapshot.data.value[key]);
}
This is one more approach to tackle the problem.
The idea is to use a variable _loading and set it to true initially.
Now, after in your inputData() function, you can set it to false once you get the dbref.
Store dbref, the way I stored _myFuture in the code below i.e., globally within the class.
Use your _loading variable to return a progress bar if its true else return FutureBuilder with your dbref.once() in place. Now, that you have loaded it, it should be available at this point.
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Is the future being loaded?
bool _loading;
// This is the future we will be using in our FutureBuilder.
// It is currently null and we will assign it in _loadMyFuture function.
// Until assigned, we will keep the _loading variable as true.
Future<String> _myFuture;
// Load the _myFuture with the future we are going to use in FutureBuilder
Future<void> _loadMyFuture() async {
// Fake the wait for 2 seconds
await Future.delayed(const Duration(seconds: 2));
// Our fake future that will take 2 seconds to return "Hello"
_myFuture = Future(() async {
await Future.delayed(const Duration(seconds: 2));
return "Hello";
});
}
// We initialize stuff here. Remember, initState is called once in the beginning so hot-reload wont make flutter call it again
#override
initState() {
super.initState();
_loading = true; // Start loading
_loadMyFuture().then((x) => setState(() => _loading = false)); // Set loading = false when the future is loaded
}
#override
Widget build(BuildContext context) {
// If loading, show loading bar
return _loading?_loader():FutureBuilder<String>(
future: _myFuture,
builder: (context, snapshot) {
if(!snapshot.hasData) return _loader(); // still loading but now it's due to the delay in _myFuture
else return Text(snapshot.data);
},
);
}
// A simple loading widget
Widget _loader() {
return Container(
child: CircularProgressIndicator(),
width: 30,
height: 30
);
}
}
Here is the output of this approach
This does the job but, you might need to do it for every class where you require your uid.
========================================
Here is the approach I described in the comments.
// Create a User Manager like this
class UserManager {
static String _uid;
static String get uid => _uid;
static Future<void> loadUID() async {
// Your loading code
await Future.delayed(const Duration(seconds: 5));
_uid = '1234'; // Let's assign it directly for the sake of this example
}
}
In your welcome screen:
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _loading = true;
#override
void initState() {
super.initState();
UserManager.loadUID().then((x) => setState(() => _loading = false));
}
#override
Widget build(BuildContext context) {
return _loading ? _loader() : Text('Welcome User ${UserManager.uid}!');
}
// A simple loading widget
Widget _loader() {
return Container(child: CircularProgressIndicator(), width: 30, height: 30);
}
}
The advantage of this method is that once you have loaded the uid, You can directly access it like this:
String uid = UserManager.uid;
thus eliminating use of futures.
Hope this helps!

pushing new page when state changes is causing weird behaviour

im new to flutter. I am trying to push to a new page whenever my qrcode reader detects a qrcode. however upon detecting the qrcode, infinite pages are being pushed. Can anyone provide me with some advice or sample code that pushes to a new page whenever the state changes?
class _QRViewExampleState extends State<QRViewExample> {
final GlobalKey qrKey = GlobalKey(debugLabel: 'QR');
_QRViewExampleState({this.state});
var state = false;
QRViewController controller;
#override
Widget build(BuildContext context) {
if (state == true) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => Menu()),
);
// Navigation
});
}
return Scaffold(
...
);
}
void _onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) {
setState(() {
state = true;
});
});
}
first of all, this line of code:
WidgetsBinding.instance.addPostFrameCallback((_) {});
usually used when you want run statement after build finished, and called inside initState(). in your case i think you don't need it.
i recommend you to call navigator inside your QRViewController listener:
_onQRViewCreated(QRViewController controller) {
this.controller = controller;
controller.scannedDataStream.listen((scanData) {
Navigator.of(context).push(
MaterialPageRoute(builder: (ctx) => Menu(scanData)),
);
});
}