Flutter Provider and Navigator - flutter

so This is my first try with Flutter. I come from Angular where dependency injection made routing a breeze. Just injecting the Router to any service would just do the trick.
Here is my scenario for flutter app:
In '/login' page I click Login button. This calls method from Auth provider, where after validating credentials Application should be routed to '/home'.
It's just something I can't wrap my head around even though I read all possible threads in the freaking internet. Everybody said one shoud wrap MaterialApp with ChangeNotifierProvider which I did - what am I doing wrong?
Here is my code. Can you point me to the right direction?
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => Auth(context: context),
child: MaterialApp(
initialRoute: "/login",
routes: {
"/login": (context) => LoginPage(),
"/home": (context) => HomePage()
},
),
);
}
}
//This is my Login page Widget - After successful login I want to be able to use Navigator to push to '/home' route
class LoginPage extends StatelessWidget {
#override
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Login"),
),
body: ElevatedButton(
//
onPressed: () => {
// here I want to use login method on Auth provider. Which should validate credentials and if valid redirect to home page
Provider.of<Auth>(context, listen: false).login()
},
child: Text("login"),
));
}
}
//This is my Home page Widget
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Scaffold(appBar: AppBar(title: Text("Home"))),
);
}
}
//This is my auth controller where After successful login I want to redirect to HomePage
class Auth extends ChangeNotifier {
BuildContext context;
Auth({
required this.context,
});
// This is login method of Auth provider -
login() {
//Here after validating the credentials (eg user / password) I want to redirect to '/home' route.
Navigator.of(context).pushNamed("/home");
}
}

I have a better solution to change your pages route based on authentication status, do code like this in your route table in MaterialApp:
route:{
'/': (context) => Consumer<AuthProvider>(
builder: (context, value, child) => value.isAuth()
? yourMainPageAfterAuth()
: AuthScreen()}
single slash will be considered as your home page, so validate your authentication in your auth provider then route to your home page after authentication
Navigator.of(context).pushNamed('/');

Related

Flutter App Back button is closing the App instead of navigating to the previous screen in flutter Navigation 2.0 with bloc and cubit

I am working on a flutter App and managing state using bloc architecture and cubits for navigation. The App is a bit large and complicated. The issue I am having is when I tap the Android back button; it closes the App instead of poping to the previous screen. I understand I have to use Route delegates and other custom navigation setup but not sure if this could be the right solution or simply fit in my app architecture.
This is the structure of my App. There four different user groups. Each user group is redirected to own dashboard after login. All user groups are sharing the AppNavigator and AuthNavigator at entry point. But after login, a use can only access his dashboard and routes there under.
AppNavigator ---> Is user logged in? if true redirect to session navigator else redirect to auth navigator. The app navigator checks if the user is authenticated and if yes redirects to switchboard which will check the user type and then redirect to appropriate dashboard. Each user group navigation flow have a high level navigation cubit the handles the navigation.
Here is part of my code:
//main.dart
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
String appTitle = 'App Title';
AuthRepository authRepository = AuthRepository();
SessionCubit sessionCubit = SessionCubit();
return MultiBlocProvider(
providers: [
BlocProvider<AuthCubit>(
create: (context) => AuthCubit(sessionCubit: sessionCubit),
),
BlocProvider<UserGroup1Cubit>(
create: (context) => UserGroup1Cubit(),
),
BlocProvider<UserGroup2Cubit>(
create: (context) => UserGroup2Cubit(),
),
...
],
child: MaterialApp(
title: appTitle,
debugShowCheckedModeBanner: false,
theme: lightThemeData(context),
darkTheme: darkThemeData(context),
home: MultiRepositoryProvider(
providers: [
RepositoryProvider<AuthRepository>(
create: (context) => AuthRepository()),
],
child: BlocProvider(
create: (context) =>
SessionCubit(authRepo: context.read<AuthRepository>()),
child: AppNavigator()),
)));
//AppNavigator.dart
//This navigator will redirect to authnavigator if the user is unauthenticated or SwitchBoard() of authenticated passing in user type from the state
class AppNavigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<SessionCubit, SessionState>(builder: (context, state) {
return Navigator(
// key:navigatorKey,
pages: [
//show loading screen
if (state is UnknownSessionState)
MaterialPage(child: SplashScreen()),
//show auth flow
if (state is UnauthenticatedState)
MaterialPage(
child: BlocProvider(
create: (context) =>
AuthCubit(sessionCubit: context.read<SessionCubit>()),
child: AuthNavigator(),
)),
//show session flow
if (state is AuthenticatedState)
MaterialPage(
child: BlocProvider(
create: (context) => SessionCubit(authRepo: AuthRepository()),
child: SwitchBoard(userType: state.user!.type),
)),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
return true;
});
});
}
}
//switchboard.dart
class SwitchBoard extends StatelessWidget {
final String? userType;
SwitchBoard({Key? key, this.userType}) : super(key: key);
#override
Widget build(BuildContext context) {
return BlocBuilder<SessionCubit, SessionState>(builder: (context, state) {
return Navigator(
pages: [
if (userType == 'group1')
MaterialPage(
child: BlocProvider(
create: (context) => Group1Cubit(
sessionCubit: context.read<SessionCubit>()),
child: UserGroup1Navigator())),
if (userType == 'group2') MaterialPage(child: UserGroup2Navigator()),
...
],
onPopPage: (route, result) {
return route.didPop(result);
},
);
});
}
}
As indicatted in the code, each user group have its own navigator and navigation cubit
//UserGroup1Navigator
class UserGroup1Navigator extends StatelessWidget {
#override
Widget build(BuildContext context) {
return BlocBuilder<Group1Cubit, Group1State>(builder: (context, state) {
return Navigator(
pages: [
//show show group dashboard
if (state is Group1DashboardState)
MaterialPage(
key: const ValueKey('Group1Dashboard'),
child: Group1Dashboard()),
if (state is ProfileState)
MaterialPage(
key: const ValueKey('UserProfile'),
child: UserProfile()),
...
],
onPopPage: (route, result) => route.didPop(result),
);
});
}
}
Each user group have its navigator trigered by navigation cubit methods and is working fine via the BlocProvider like so:
//Navigate to UserProfile from anywhere in the app
BlocProvider.of<Group1Cubit>(context).showGroup1UserProfile();
The only problem is that cannot navigate back by pressing the back button on Android device. This will close the App instead. Any work around on this will appreciate.

Flutter screen not changing after provider state changed

I'm new in flutter and trying to understand flutter state management concept using provider. This the image scenario what I'm trying to do
I have created a file called auth_provider.dart file under the folder called Providers
class AuthProvider with ChangeNotifier{
bool isLogin = false;
Future createUser() async
{
isLogin = true;
notifyListeners();
}
Future login() async
{
isLogin = true;
notifyListeners();
}
void logout()
{
isLogin = false;
notifyListeners();
}
}
This the Signup button that I have created in the login page
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SignupPage()
),
);
},
child: const Text(
'Signup Button',
),
)
This is the signUp button in signup screen
child: ElevatedButton(
onPressed: () => signUpSubmit(),
child: const Text(
'Sign Up',
),
),
I have written a signUpSubmit future like below
Future<void> signUpSubmit() async {
Provider.of<AuthProvider>(context, listen: false).createUser();
}
I have used AuthProvider consumer in main.dart page
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) => AuthProvider(),
),
],
child: Consumer<AuthProvider>(
builder: (ctx,auth,child){
print(auth.isLogin);
return MaterialApp(
home: auth.isLogin ? const HomeScreen():const LoginPage(),
routes: {
HomeScreen.routeName: (ctx) => const HomeScreen(),
SignupPage.routeName: (ctx) => const SignupPage(),
LoginPage.routeName: (ctx) => const LoginPage(),
},
);
}
),
);
}
}
After click on signup button I'm getting true in main page , which I have given a print under Consumer builder in main.dart page. So according to MaterialApp widget home condition page should redirect to HomeScreen but it's not moving. Why it's not moving ? What is the main cause and what it the best way to solve this problem ?
Note : If I try it from login screen redirection is working fine. But according to my image flow (Login -> signup) it's not working.
here is the code you are looking for, but bear in mind with the implementation you have right now, if the user opens the app again, it will redirect them to the signin page. because the boolean value will disappear once the user closes the app.
change your main.dart file like the following..
main function
void main() {
// you just need to add the multiprovider and the change notifier provider class
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => AuthProvider()),
],
child: const MyApp(),
),
);
}
here is the MyApp class as i understand it.
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (ctx, auth, child) {
print(auth.isLogin);
return MaterialApp(
home: auth.isLogin ? MyHomePage() : LoginPage(),
routes: {
MyHomePage.routeName: (ctx) => MyHomePage(),
LoginPage.routeName: (ctx) => LoginPage(),
//NavScreen.routeName: (ctx) => const NavScreen(),
},
);
});
}
}
Change the signup button in the register page to the following.
ElevatedButton(
onPressed: () {
signUpSubmit(context);
Navigator.of(context).pushNamed(HomeScreen.routeName);
},
and the signupsubmit function like this..
signUpSubmit(BuildContext context) {
Provider.of<AuthProvider>(context, listen: false).createUser();
}
The main cause of your problem is that you are pushing a new route (screen) from login page and the best way to solve problem is to pop that route (screen) from sigupPage.
On click of Signup button from login page you are pushing a new route, so in order to redirect to HomeScreen from SignupPage first you need to pop that route so that you can see the updated changes.
Future<void> signUpSubmit() async {
Navigator.of(context).pop();
Provider.of<AuthProvider>(context, listen: false).createUser();
}
https://docs.flutter.dev/cookbook/navigation/navigation-basics

Provider login logout flutter

I am trying to create a simple authentication flow using Provider. I have three pages :
LoginPage
OnboardingPage
HomePage
The flow of this app is:
if a user opens the app for the first time, he/she will be redirected to the onboarding then to login to home.
For the second time user, the app first checks the login status and redirected to either log in -> home or straight to home page.
Here is my setup in code :
main.dart
void main() {
runApp(MultiProvider(providers: [
ChangeNotifierProvider<StorageHelper>(create: (_) => StorageHelper()),
ChangeNotifierProvider<AuthProvider>(create: (_) => AuthProvider()),
], child: MyApp()));
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return Consumer<AuthProvider>(builder: (final BuildContext context,
final AuthProvider authProvider, final Widget child) {
print(authProvider.isAuthenticated); // this is false whenever I //click the logout from category(or other pushed pages) but the below ternary //operation is not executing
return MaterialApp(
title: 'My Poor App',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primaryColor: Color(0xff29c17e),
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: authProvider.isAuthenticated ? HomeScreen() : LoginScreen(),
onGenerateRoute: Router.onGenerateRoute,
);
});
}
}
LoginScreen.dart
class LoginScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final authProvider = Provider.of<AuthProvider>(context, listen: false);
return Scaffold(
body: Center(
child: MaterialButton(
onPressed: () async {
await authProvider.emailLogin('user#email.com', 'pass');
},
child: Text('Login'))),
);
}
}
HomeScreen.dart
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final auth = Provider.of<AuthProvider>(context, listen: false);
return Scaffold(
body: Center(
child: MaterialButton(
elevation: 2,
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CategoryScreen()));
},
child: Text('Reset')),
),
);
}
}
AuthProvider.dart
class AuthProvider extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
set isAuthenticated(bool isAuth) {
_isAuthenticated = isAuth;
notifyListeners();
}
Future emailLogin(String email, String password) async {
isAuthenticated = true;
}
Future logout() async {
isAuthenticated = false;
}
}
If i logout from home page using Provider.of<AuthProvider>(context).logout() it works fine. But if I push or pushReplacement a new route and try to logout from the new route (just say I navigated from home to category page and try to logout from there), I am not redirected to LoginPage. If I print the value of isAuthenticated it prints false but the consumer is not listening or at least not reacting to the variable change.
Please don't mark this question as duplicate, I have searched many other similar questions and none of them worked for my case.
Edit:
CategoryScreen.dart
class CategoryScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: RaisedButton(
onPressed: () {
final auth = Provider.of<AuthProvider>(context, listen: false);
auth.logout();
// print(auth.isAuthenticated);
},
child: Text('Category Logout'),
),
),
);
}
}
I guess your problem is that you did not use Consumer for the logout, in your home in the MaterialApp. Just see, that if it works out for you
main.dart
// needs to listen to the changes, to make changes
home: Consumer<AuthProvider>(
builder: (context, authProvider, child){
return authProvider.isAuthenticated ? HomeScreen() : LoginScreen();
}
)
Since, Consumer was not there for your home, even if the value was being changed, it was not able to work on updating the view for you as per the Provider.

Accessing InheritedWidget in widget A's build method leads to rebuild of widget A which leads to infinite cycle

I want to make an app which connects to server and sends it different requests, gets responses etc. For that I have a Client class (not a widget), which handles all the network stuff. I use bloc pattern to marshal requests to Client and get back responses. This Client instance should be accessible all over the widget tree, so I use InheritedWidget. My idea is that the first thing user sees after opening an app is splash screen, which should change to some other page when connection to server is established. I subscribe to response stream in SplashScreen's build method and navigate to different page when response is received, but for some reason accessing InheritedWidget triggers rebuild of SplashScreen which leads to accessing InheritedWidget which leads to rebuild and so on and so forth. And obviously stream starts complaining that I already subscribed to it. I do not change InheritedWidget anywhere, and updateShouldNotify is not called, and I set it to return false anyway. Here is minimal reproducible example. It obviously does not perform any real network communication, but it demonstrates what I try to say.
var client = ClientInheritedData.of(context).client leads to rebuild. If I comment it and lines that use client, rebuild is not triggered.
class Client {
StreamController<int> _eventStreamController;
StreamSink<int> get eventSink => _eventStreamController.sink;
Stream<int> _eventStream;
StreamController<String> _responseStreamController;
StreamSink<String> _responseSink;
Stream<String> get responseStream => _responseStreamController.stream;
Client() {
_eventStreamController = StreamController();
_eventStream = _eventStreamController.stream;
_responseStreamController = StreamController<String>();
_responseSink = _responseStreamController.sink;
_eventStream.listen((event) async {
if (event == 1) {
await Future.delayed(Duration(seconds: 2)); // simulate some work
_responseSink.add('Connected!');
}
});
}
}
void main() => runApp(ClientInheritedData(Client(), MyApp()));
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
primarySwatch: Colors.green,
),
initialRoute: '/',
onGenerateRoute: RouteGenerator.generate,
);
}
}
class SplashScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
var client = ClientInheritedData.of(context).client;
client.eventSink.add(1);
client.responseStream.listen((response) {
Navigator.of(context).pushNamed('/Sign Up');
});
return Scaffold(
appBar: AppBar(
title: Text('Splash Screen'),
),
body: Center(
child: Text('Greetings!'),
),
);
}
}
class SignUp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sign Up'),
),
body: Center(
child: Text('Hey man'),
),
);
}
}
class ClientInheritedData extends InheritedWidget {
final Client client;
const ClientInheritedData(this.client, Widget child) : super(child: child);
static ClientInheritedData of(BuildContext context) {
return context.inheritFromWidgetOfExactType(ClientInheritedData) as ClientInheritedData;
}
#override
bool updateShouldNotify(InheritedWidget oldWidget) {
return false;
}
}
class RouteGenerator {
static Route<dynamic> generate(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(
builder: (context) => SplashScreen(),
);
case '/Sign Up':
return MaterialPageRoute(
builder: (context) => SignUp(),
);
}
}
}

How to navigate to another page on load in Flutter

I want to navigate to the login page if there is no logged in user, otherwise display the homepage. I thought of calling Navigator.of(context).push() conditionally inside the build method but that triggers an exception. Is there some method I'm missing that I can override?
Update to add the Homepage widget
class HomePage extends StatelessWidget {
final AppUser user;
const HomePage({Key key, this.user}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Rera Farm'),
actions: <Widget>[
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry>[
PopupMenuItem(
child: ListTile(
title: Text('Settings'),
onTap: () {
Navigator.pop(context);
Navigator.push(context,
MaterialPageRoute(builder: (BuildContext context)
=> SettingsPage()
));
},
),
),
];
},
)
],
),
body: _buildBody(context));
}
And the container
class HomePageContainer extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (BuildContext context, _ViewModel vm) {
return HomePage(
user: vm.user,
);
},
);
}
}
You need to either use a ternary in the onTap if you're using the settings button or, if you just want it to automatically send the user to the correct page when the app starts, you can put the ternary in the MyApp build method.
If you are using the settings button and just want it to pop back to the previous page if the person is not logged in then you can change NotLoggedIn() to a pop.
For some strange reason SO is refusing to post the code when it is properly formatted with four spaces, exactly as it asks, so I'm just going to make a gist.
https://gist.github.com/ScottS2017/3288c7e7e9a014430e56dd6be4c259ab
Here's how I end up doing it. I do the checks in the main method, so the user sees the splash screen set in manifest while those weird checks are made:
void main() {
WidgetsFlutterBinding.ensureInitialized();
SharedPreferences.getInstance().then((instance) {
_token = instance.getString("token");
final _loggedIn = _token != null && token != "";
runApp(MyApp(loggedIn: _loggedIn));
});
}
Then in your app add the parameters to switch:
class MyApp extends StatelessWidget {
final bool loggedIn;
MyApp({this.key, this.loggedIn});
#override
Widget build(BuildContext context) {
return MaterialApp(
home: loggedIn ? HomePage() : LoginPage(),
);
}
}
You can also use Navigator.pushReplacement() if you need to do it below MyApp(). Just posting it here for future generations.