Provider is not working when navigate to new screen - flutter

I implemented Authentication by provider
The problem is when is the first time myHomeCalss is notified that the user is Authenticated by dont return the correctPage (MainGui)
SplashPages is page with a button continue, and push the login page ,
The Login page is pushed outside of costumer
but when I dont pass in the SplashPages is worked perfectyl
any adea please
//splash page
ContinueButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
ListenableProvider.value(
value: yourModel,
child: LoginPage(),
),
),
);
}
)
//main
void main() async {
setupLocator();
WidgetsFlutterBinding.ensureInitialized();
await firebase_core.Firebase.initializeApp();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthenticationService()),
],
child: MyApp(),
),
);
}
//My app
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHome(),
builder: (context, child) => Navigator(
key: locator<DialogService>().dialogNavigationKey,
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => DialogManager(child: child)),
));
}
}
MyHome
Class MyHome extends StatelessWidget {
#override
Widget build(BuildContext context) {
return SafeArea(
child: FutureBuilder<bool>(
future: startTime(),
builder: (BuildContext context, AsyncSnapshot<bool> snapshot2) {
if (snapshot2.hasData) {
if (snapshot2.data) {
return SplashPages();
} else {
return Consumer<AuthenticationService>(builder: (_, auth, __) {
if (auth.currentUserr == null) {
return LoginPage();
} else {
return FutureBuilder(
future: auth.populateCurrentUser(auth.currentUserr),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (auth.currentUserr.emailVerified) {
return MainGui();
} else {
return ValidationMailPage(
email: auth.currentUserr.email,
);
}
} else
return Container(
// child: Center(
// child: SpinKitRotatingCircle(
// color: Colors.white,
// size: 50.0,
// ))
);
});
}
});
}
}

You may consider using SharedPreferences, in which you will store the user (or maybe just the token), and then check in main if there is a token/user stored there before rendering the app; if there is a token you log in and then push to the homepage, if not you navigate directly to the login page.
SharedPrefenreces is persisted data storage that persists even if you restart the app, but Provider is a state management solution that doesn't persist between app restarts.
Here is the SharedPreferences plugin you may use.

Related

Opening a screen out the result of a statement

enter code hereI want to open a screen to add extra information if it is not set yet. So after the user is logged in I check if the extra info is set. If not I want it to go to a screen to fill in the info. If the user is done it should go to a "Homescreen". If the user info is already set it should immediately go to the home screen.
I already tried to just go to the extra info form and then Navigator.push to the home screen but then it has difficulties with logging out. I searched for a long time but can not find anything.
class CampPage extends StatelessWidget {
final String email;
final String uid;
const CampPage({super.key, required this.email, required this.uid});
#override
Widget build(BuildContext context) {
return FutureBuilder(
// ignore: unrelated_type_equality_checks
future: context.read<UserProvider>().exists(uid) == true
? null
: Future.delayed(Duration.zero, () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NewUserPage(email: email, userId: uid),
),
);
}),
builder: (context, snapshot) => Scaffold(
drawer: const DrawerHomePage(),
appBar: AppBar(
title: const Text("Camp Page"),
),
body: Column(
children: const [
Text("nieuwe features"),
],
),
),
);
}
}
this is one of the things I try but the NewUserPage always pops up and I only want it to pop up if context.read<UserProvider>().exists(uid) == false
also the solution mentioned does not work for me. I think because there is a screen in between the login and logout (The form screen) the logout function does not work properly.
`
class UserPage extends StatelessWidget {
const UserPage({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: const Text("Submit"),
onPressed: () {
//Log out of Firestore Authentication
},
),
);
}
}
class NewForm extends StatelessWidget {
const NewForm({super.key});
#override
Widget build(BuildContext context) {
return Scaffold(
body: ElevatedButton(
child: const Text("Submit"),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const UserPage()),
);
},
),
);
}
}
Widget build(BuildContext context) {
return FutureBuilder(
future: context.read<UserProvider>().exists(uid)
builder: (context, snapshot) {
if (snapshot.hasdata){
if (snapshot.data == true) {
return const UserPage();
} else {
return const NewForm();
}
}
else // show a proggress bar
}
);
}
`
Does someone still have another solution?
I think you should do this:
Widget build(BuildContext context) {
return FutureBuilder(
future: context.read<UserProvider>().exists(uid)
builder: (context, snapshot) {
if (snapshot.hasdata){
if (snapshot.data == true) // then the user exist
else // the user doesn't exist
}
else // show a proggress bar
}
);
}

Flutter bloc - do not call the api every time (use the same State in different views)

I'm using flutter bloc pattern in my flutter app. I have bottom navigation bar with several tabs in one page. Two of them are using the same api call (the same State). When the user taps on 1 of them I call the api to get the data, but if the user taps on the other tab I want to get the data without calling the api again. How I can do that?
In my main page (dashboard) I have BlocBuilder to change the tabs and I create the Dashboard cubit in it
class DashboardPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<TabsBloc, AppTab>(
builder: (BuildContext context, AppTab activeTab) {
return Scaffold(
appBar: AppBar(
title: Text(DashboardHelpers.getTabLabel(activeTab)),
),
body: RepositoryProvider(
create: (BuildContext context) => DashboardRepository(),
child: BlocProvider<DashboardCubit>(
create: (BuildContext context) => DashboardCubit(
dashboardRepository: context.read<DashboardRepository>(),
authBloc: context.read<AuthBloc>(),
),
child: DashboardHelpers.getTabContent(activeTab),
),
),
bottomNavigationBar: TabSelector(
activeTab: activeTab,
onTabSelected: (tab) =>
BlocProvider.of<TabsBloc>(context).add(TabUpdated(tab))),
);
},
);
}
}
The tabs are View that are loaded as child. One of the views is View1. When I get the data I loaded in ContentView1
class View1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
BlocProvider.of<DashboardCubit>(context)..getDashboardDevices();
return BlocConsumer<DashboardCubit, DashboardState>(
listener: (BuildContext context, DashboardState state) {
if (state is DashboardError) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(state.message),
),
);
}
},
builder: (BuildContext context, DashboardState state) {
if (state is DevicesLoaded) {
return ContentView1(data: state.data);
} else if (state is DashboardLoading) {
return LoadingWidget();
} else if (state is DashboardError) {
return Container(
child: Center(
child: Text(state.message),
),
);
} else {
return Container();
}
},
);
}
}
and the View2 is almost the same. The data is absolutely the same and it is loaded in ContentView2 but it is a completly different widget than ContentView1
class View2 extends StatelessWidget {
#override
Widget build(BuildContext context) {
BlocProvider.of<DashboardCubit>(context)..getDashboardDevices();
return BlocConsumer<DashboardCubit, DashboardState>(
listener: (BuildContext context, DashboardState state) {
if (state is DashboardError) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(state.message),
),
);
}
},
builder: (BuildContext context, DashboardState state) {
if (state is DevicesLoaded) {
return ContentView2(data: state.data);
} else if (state is DashboardLoading) {
return LoadingWidget();
} else if (state is DashboardError) {
return Container(
child: Center(
child: Text(state.message),
),
);
} else {
return Container();
}
},
);
}
}
The problem is that these two VIEWs are showing different data that comes from the same API endpoint.
How can I load the already gotten data when the user tabs from View1 to View2 without calling the API again.
Thanks!
You should be calling getDashboardDevices() only once, for that you could create a DashboardInitialState, when user clicks one of tabs, if the state is DashboardInitialState you run getDashboardDevices() and not always when the view is building. This way you will load data only once when one of the views is built and both of them will use the same data on loaded state.
There is View1 as example, try that with both views:
class View1 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocConsumer<DashboardCubit, DashboardState>(
listener: (BuildContext context, DashboardState state) {
if (state is DashboardError) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text(state.message),
),
);
}
},
builder: (BuildContext context, DashboardState state) {
if(state is DashboardInitialState) {
BlocProvider.of<DashboardCubit>(context)..getDashboardDevices();
return LoadingWidget();
} else if (state is DevicesLoaded) {
return ContentView1(data: state.data);
} else if (state is DashboardLoading) {
return LoadingWidget();
} else if (state is DashboardError) {
return Container(
child: Center(
child: Text(state.message),
),
);
} else {
return Container();
}
},
);
}
}

How to switch between Auth screen and Home screen based on bool value?

I want to switch between the login screen and Home screen based on bool value(user.status) from the model class below
class User extends ChangeNotifier {
int phoneNumber;
bool status = false;
notifyListeners();
}
The bool User.status value is flipped from below function
User _user = Provider.of<User>(context);
...
...
if (form.validate()) {
_user.status = true;
}
The below function has to listen to the changes in the status value from the User model and change the screen to Home().
class Wrapper extends StatelessWidget {
#override
Widget build(BuildContext context) {
User authStatus = Provider.of<User>(context);
return authStatus.status ? Home() : Auth();
}
}
I don't have any errors, all the values are updating accordingly but the Wrapper() is not being rebuilt after listening to the changes from ChangeNotifier
Here's how I do it with Provider :
routes: {
"/": (context) => MainPage(),
"/detail": (context) => UserDetailPage(),
},
builder: (context, child) {
return Consumer<UsersProvider>(
child: child,
builder: (context, provider, child) {
final value = provider.user;
if (!value.status) {
return Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
settings: settings, builder: (context) => LoginPage()),
);
}
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => UsersProvider()),
ChangeNotifierProvider(
create: (context) => InvoicesProvider()),
ChangeNotifierProvider(create: (context) => EventsProvider()),
],
child: child,
);
},
);
},
Basically use builder in main.dart and defines routes, then inside builder use Consumer were child is the initial route MainPage() so if the user already login they will go there, and if not, base on status they will redirect to LoginPage(). I hope you can understand feel free to comment

Flutter Firebase Authentication: delay on startup

I am using Provider and the stream FirebaseAuth.instance.onAuthStateChanged in the app to decide where to redirect on startup, but although the user is already logged in (from a previous startup) the app starts on the login screen and almost 1 second later redirects to the home page, from which it should have started from the first moment. This happens even in airplane mode.
I would like to know if there is any approach to solve this, even if it is not possible to show the home screen at once, I don't know how to differentiate between the not logged user (null->login screen) and loading user (null->loading screen).
Some of the code:
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final FirebaseAuth _auth = FirebaseAuth.instance;
final DatabaseService db = DatabaseService();
#override
Widget build(BuildContext context) {
return StreamProvider<FirebaseUser>.value(
value: _auth.onAuthStateChanged,
child: Consumer<FirebaseUser>(
builder: (context, firebaseUser, child) {
return MultiProvider(
providers: [
if (firebaseUser != null)
ChangeNotifierProvider(create: (ctx) => CollectionState(firebaseUser)),
StreamProvider<List<Collection>>.value(value: db.streamCollections(firebaseUser)),
],
child: MaterialApp(
title: 'My App',
routes: {
'/': (ctx) => LandingPage(),
'/login': (ctx) => LoginPage(),
'/emailSignIn': (ctx) => EmailSignInPage(),
'/emailSignUp': (ctx) => EmailSignUpPage(),
'/emailUnverified': (ctx) => EmailUnverifiedPage(),
'/home': (ctx) => HomePage(),
'/settings': (ctx) => Settings(),
},
),
);
},
),
);
}
}
class LandingPage extends StatelessWidget {
final DatabaseService _db = DatabaseService();
#override
Widget build(BuildContext context) {
final user = Provider.of<FirebaseUser>(context);
final userCondition =
user == null ? 'null' : user.isEmailVerified ? 'verifiedUser' : 'unverifiedUser';
switch (userCondition) {
case 'null':
return LoginPage();
break;
case 'unverifiedUser':
return EmailUnverifiedPage();
break;
case 'verifiedUser':
return HomePage();
break;
}
}
}
The code is a bit simplified, I use a service for the authentication instance instead, just that.
I know I'm very late, but I've had the same problem for weeks and I finally figured it out.
#ChinkySight is right when he says it's best to use a StreamBuilder, mostly because you have access to the connectionState property.
The reason why lag exists is because the connection to the stream is not fully established. So during ConnectionState.waiting, return a widget like a splash screen or just a container.
class Home extends StatelessWidget {
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_, snapshot) {
// Added this line
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
if (snapshot.data is FirebaseUser && snapshot.data != null) {
return HomePage();
}
return LoginPage();
});
}
}
You can even give your return statements fancy animations with the Animated Switcher
return StreamBuilder(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
Widget widget;
if (snapshot.connectionState == ConnectionState.waiting) {
return Container();
}
switch (snapshot.hasData) {
case (true):
widget = HomePage();
break;
case (false):
widget = LoginPage();
}
return Stack(
children: <Widget>[
Scaffold(
backgroundColor: Colors.grey.shade200,
),
AnimatedSwitcher(
duration: Duration(milliseconds: 700),
child: FadeTransition(
opacity: animation,
child: widget,
),
);
},
)
],
);
},
);
This works for FlutterFire.
Firebase Auth enables you to subscribe in realtime to this state via a
Stream. Once called, the stream provides an immediate event of the
user's current authentication state, and then provides subsequent
events whenever the authentication state changes. To subscribe to
these changes, call the authStateChanges() method on your FirebaseAuth
instance:
import 'package:firebase_auth/firebase_auth.dart' as auth;
import 'package:flutter/material.dart';
import 'menu.dart';
import 'login.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:firebase_core/firebase_core.dart';
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(
MyApp()
);
}
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp, DeviceOrientation.portraitUp]);
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'TestApp',
theme: ThemeData(primarySwatch: Colors.blue),
home:
StreamBuilder<auth.User>(
stream: auth.FirebaseAuth.instance.authStateChanges(),
builder: (BuildContext context, AsyncSnapshot<auth.User> snapshot) {
if(snapshot.hasData) {
print("data exists");
return HomePage();
}
else {
return LoginPage();
}
},
)
);
}
}

Flutter: Dynamic Initial Route

Dears,
I am using provider dart package which allows listeners to get notified on changes to models per se.
I am able to detect the change inside my main app root tree, and also able to change the string value of initial route however my screen is not updating. Kindly see below the code snippet and the comments lines:
void main() => runApp(_MyAppMain());
class _MyAppMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>.value(
value: UserProvider(),
),
ChangeNotifierProvider<PhoneProvider>.value(
value: PhoneProvider(),
)
],
child: Consumer<UserProvider>(
builder: (BuildContext context, userProvider, _) {
return FutureBuilder(
future: userProvider.getUser(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
final User user = snapshot.data;
String initialScreen = LoginScreen.path;
// (1) I am able to get into the condition
if (user.hasActiveLogin()) {
initialScreen = HomeOneScreen.path;
}
return MaterialApp(
title: 'MyApp',
theme: ThemeData(
primarySwatch: Colors.green,
accentColor: Colors.blueGrey,
),
initialRoute: initialScreen,
// (2) here the screen is not changing...?
routes: {
'/': (context) => null,
LoginScreen.path: (context) => LoginScreen(),
RegisterScreen.path: (context) => RegisterScreen(),
HomeOneScreen.path: (context) => HomeOneScreen(),
HomeTwoScreen.path: (context) => HomeTwoScreen(),
RegisterPhoneScreen.path: (context) => RegisterPhoneScreen(),
VerifyPhoneScreen.path: (context) => VerifyPhoneScreen(),
},
);
},
);
},
),
);
}
}
Kindly Note the Below:
These are are paths static const strings
LoginScreen.path = "login"
RegisterScreen.path = "/register-screen"
HomeOneScreen.path = "home-one-screen"
HomeTwoScreen.path = "home-two-screen"
RegisterPhoneScreen.path = "/register-phone-screen"
VerifyPhoneScreen.path = "/verify-phone-screen"
What I am missing for dynamic initialRoute to work?
Many Thanks
According to this issue described on github issues it is not permissible to have initial route changes. At least this is what I understood. However what I did is that I replaced the initialRoute attribute with home attr. Thus this change mandates that initialScreen becomes a widget var.
The changes is shown below:
void main() => runApp(_MyAppMain());
class _MyAppMain extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<UserProvider>.value(
value: UserProvider(),
),
ChangeNotifierProvider<PhoneProvider>.value(
value: PhoneProvider(),
)
],
child: Consumer<UserProvider>(
builder: (BuildContext context, userProvider, _) {
return FutureBuilder(
future: userProvider.getUser(),
builder: (BuildContext context, AsyncSnapshot<User> snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
final User user = snapshot.data;
// (1) This becomes a widget
Widget initialScreen = LoginScreen();
if (user.hasActiveLogin()) {
initialScreen = HomeOneScreen();
}
return MaterialApp(
title: 'MyApp',
theme: ThemeData(
primarySwatch: Colors.green,
accentColor: Colors.blueGrey,
),
home: initialScreen,
// (2) here the initial route becomes home attr.
routes: {
'/': (context) => null,
LoginScreen.path: (context) => LoginScreen(),
RegisterScreen.path: (context) => RegisterScreen(),
HomeOneScreen.path: (context) => HomeOneScreen(),
HomeTwoScreen.path: (context) => HomeTwoScreen(),
RegisterPhoneScreen.path: (context) => RegisterPhoneScreen(),
VerifyPhoneScreen.path: (context) => VerifyPhoneScreen(),
},
);
},
);
},
),
);
}
}
Also note on my RegistrationScreen on success api response I did Navigator.of(context).pop()
Thanks