I am developing by flutter for web.
It is currently possible to go from the main page to the profile page, but is it possible to keep those URLs separate?
For example:
Main page : https:// testapp.com/home
Profile page: https:// testapp.com/profile
When I execute the profilePage() function, Navigator.push will take me to the Profile page.
void profilePage() {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => showProfile(),
),
);
}
showProfile is executed with a different dart file to complete the page navigation.
class showProfile extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
title: const Center(
child: Text("Test App",
style:
TextStyle(color: Colors.black)),
)),
body: Container(
height: double.infinity,
color: Colors.white,
),
);
}
}
Currently, the Main page and Profile page have the same URL (https:// testapp.com/#/).
Therefore, even if the user types testapp.com/profile, they cannot jump to the profile page.
My purpose is to go to a different URL in Navigator.
Thank you.
Add this dependancy/plugin.
Now, Create a file with name routing.dart and add this code:-
class FluroRouting {
static final router = FluroRouter();
static Handler _profileHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
Profile());
static Handler _homeHandler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) =>
HomePage());
static void setupRouter() {
router.define('/home', handler: _homeHandler,);
router.define('/profile', handler: _profileHandler,);
router.notFoundHandler = Handler(
handlerFunc: (BuildContext? context, Map<String, dynamic> params) =>HomePage()
);
}
static void navigateToPage({String routeName,BuildContext context}) {
router.navigateTo(context, routeName, transition: TransitionType.none);
}
static void pushAndClearStackToPage({String routeName,BuildContext context}) {
router.navigateTo(context, routeName, clearStack: true,transition: TransitionType.none);
}
}
Now you need to initialize and setup these routes so modify your void main as:-
void main()async {
FluroRouting.setupRouter();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'My Website',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/home',
onGenerateRoute: FluroRouting.router.generator,
);
}
}
And whenever you need to navigate then simply use the below code:-
FluroRouting.navigateToPage(routeName:"/profile",context:context);
This web site helped my issue.
https://medium.com/flutter/flutter-web-navigating-urls-using-named-routes-307e1b1e2050
Solution;
Write the following code in the main function.
void main() {
runApp(MaterialApp(
initialRoute: '/',
routes: {
'/profile': (context) => ProfilePage(),
},
title: "test",
home: (),
));
}
Declare the following in the widget whose URL you want to be "domain.com/ profile" (To be exact, "domain.com/#/profile").
class ProfilePage extends StatelessWidget {
static const String route = '/profile';
}
And finally, call this.
onPressed: () {
Navigator.of(context).pushNamed(ProfilePage.route);
},
Related
Basically, I want to return an OTP (One Time Pin) input screen if the app opens and there isn't a valid user file, otherwise the app should open with a menu (Using Provider).
What I tried was:
Main calls runApp(const MyApp()); which is stateless.
MyApp's build returns MultiProvider with its child const AuthenticatedMaterialApp(), which is a stateful widget.
Here I tried various things , including setting different routes based on the provided values, but keep getting unexpected (for me) results; e.g. in _AuthenticatedMaterialAppState:
class _AuthenticatedMaterialAppState extends State<AuthenticatedMaterialApp> {
late ConnectionNotifier connectionNotifier;
late Authenticate auth;
late bool isUserOkay;
#override
void didChangeDependencies() {
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
connectionNotifier = context.read<ConnectionNotifier>();
auth = context.watch<Authenticate>();
isUserOkay = (auth.isActive && auth.isKnown);
developer.log('build isUserOkay:$isUserOkay isKnown: ${auth.isKnown}',
name: 'AuthenticatedMaterialApp');
String initRoute = (!isUserOkay) ? '/otp' : '/menu';
developer.log('initRoute $initRoute', name: 'AuthenticatedMaterialApp');
if (!isUserOkay) {
developer.log('isUserOkay: $isUserOkay return OTP app ', name: 'AuthenticatedMaterialApp');
return MaterialApp(
navigatorObservers: [routeObserver],
title: 'Named Routes',
theme: ThemeData(scaffoldBackgroundColor: Colors.white),
initialRoute: '/otp',
routes: {
'/otp': (context) => const OTPScreen(),
},
);
} else {
developer.log('isUserOkay: $isUserOkay return menu app ', name: 'AuthenticatedMaterialApp');
return MaterialApp(
navigatorObservers: [routeObserver],
title: 'Named Routes',
theme: ThemeData(scaffoldBackgroundColor: Colors.white),
initialRoute: '/menu',
routes: {
'/menu': (context) => const MenuScreen(),
'/stocks': (context) => const StocksRoute(),
'/stocks/stock': (context) => const StockPage(),
'/clients': (context) => const ClientsRoute(),
'/clients/client': (context) => const ClientPage(),
'/viewings': (context) => const ViewingsRoute(),
'/viewings/viewing': (context) => const ViewingPage(),
'/offers': (context) => const OffersRoute(),
'/offers/offer': (context) => const OfferPage(),
'/checkin': (context) => const CheckinScreen(),
'/calendar': (context) => const CalendarScreen(),
'/reports': (context) => const ReportsScreen(),
'/calculators': (context) => const CalculatorsScreen(),
'/test': (context) => const TestRoute(),
},
);
}
}
}
... eventually generates output that reads [AuthenticatedMaterialApp] isUserOkay: true return menu app , but what is shown on the screen is the OTP screen.
Why? - has it something to do with AuthenticatedMaterialApp being a const, or?
I also tried registering all the routes but having initialRoute: be a variable string but get basically the same result.
I can get it working by creating a separate 'home` route, and checking the user there and ~redirecting like:
class _HomeState extends State<Home> {
late Authenticate auth;
#override
Widget build(BuildContext context) {
auth = context.watch<Authenticate>();
developer.log('build', name: '_HomeScreenState');
if (auth.isKnown) {
if (auth.isActive == true) {
return const MenuScreen();
} else if (auth.isActive == false) {
return const OTPScreen();
}
}
return Container();
}
}
while I have all the routes including OTP, menu etc all registered in the AuthenticatedMaterialApp, which I suppose is fine, but I would like to understand why my initial approach doesn't/can't work?
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.
I am trying to implement custom logout solution for my application, where no matter where user currently is, once the Logout button is clicked, app will navigate back to Login page.
My idea was, that instead of listening on every component for state changes, I would have one single listener on a master component -> MyApp.
For the sake of simplicity, I have stripped down items to bare minimum. Here is how my Profile class could look like:
class Profile with ChangeNotifier {
bool _isAuthentificated = false;
bool get isAuthentificated => _isAuthentificated;
set isAuthentificated(bool newVal) {
_isAuthentificated = newVal;
notifyListeners();
}
}
Now, under Main, I have registered this provider as following:
void main() => runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (_) => Profile(),
)
],
child: MyApp(),
),
);
And finally MyApp component:
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return Consumer<Profile>(
builder: (context, profile, _) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
brightness: Brightness.light,
primaryColor: Color.fromARGB(255, 0, 121, 107),
accentColor: Color.fromARGB(255, 255, 87, 34),
),
home: buildBasePage(context, profile),
);
},
);
}
Widget buildBasePage(BuildContext context, Profile currentProfile) {
return !currentProfile.isAuthentificated
? LoginComponent()
: MyHomePage(title: 'Flutter Demo Home Page test');
}
}
My idea was, that as MyApp component is the master, I should be able to create a consumer, which would be notified if current user is authentificated, and would respond accordingly.
What happens is, that when I am in e.g. MyHomePage component and I click Logout() method which looks like following:
void _logout() {
Provider.of<Profile>(context, listen: false).isAuthentificated = false;
}
I would be expecting that upon changing property, the initial MyApp component would react and generate LoginPage; which is not the case. I have tried changing from Consumer to Provider.of<Profile>(context, listen: false) yet with the same result.
What do I need to do in order for this concept to work? Is it even correct to do it this way?
I mean I could surely update my Profile class in a way, that I add the following method:
logout(BuildContext context) {
_isAuthentificated = false;
Navigator.push(
context, MaterialPageRoute(builder: (context) => LoginComponent()));
}
And then simply call Provider.of<Profile>(context, listen: false).logout(), however I thought that Provider package was designed for this...or am I missing something?
Any help in respect to this matter would be more than appreciated.
I don't know why it wasn't working for you. Here is a complete example I built based on your description. It works!
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Profile with ChangeNotifier {
bool _isAuthentificated = false;
bool get isAuthentificated {
return this._isAuthentificated;
}
set isAuthentificated(bool newVal) {
this._isAuthentificated = newVal;
this.notifyListeners();
}
}
void main() {
return runApp(
MultiProvider(
providers: [
ChangeNotifierProvider<Profile>(
create: (final BuildContext context) {
return Profile();
},
)
],
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Consumer<Profile>(
builder: (final BuildContext context, final Profile profile, final Widget child) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: profile.isAuthentificated ? MyHomePage() : MyLoginPage(),
);
},
);
}
}
class MyHomePage extends StatelessWidget {
#override
Widget build(final BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Home [Auth Protected]")),
body: Center(
child: RaisedButton(
child: const Text("Logout"),
onPressed: () {
final Profile profile = Provider.of<Profile>(context, listen: false);
profile.isAuthentificated = false;
},
),
),
);
}
}
class MyLoginPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Login")),
body: Center(
child: RaisedButton(
child: const Text("Login"),
onPressed: () {
final Profile profile = Provider.of<Profile>(context, listen: false);
profile.isAuthentificated = true;
},
),
),
);
}
}
You don't need to pass listen:false, instead simply call
Provider.of<Profile>(context).logout()
So your Profile class would look like
class Profile with ChangeNotifier {
bool isAuthentificated = false;
logout() {
isAuthentificated = false;
notifyListeners();
}
}
I just started learning Dart/Flutter, and I have been advised to use the Fluro package for navigation.
Is it possible to switch between 2 StatefulWidget "pages" with Fluro router?
I had no problems when switching between stateless widgets, but when I've tried to make them stateful, I got this error: "The return type 'Page2' isn't a 'Widget', as defined by anonymous closure." I can't figure out what I should change in the code.
class FluroRouter {
static Router router = Router();
static void setupRouter() {
router.define("Page2", handler: page2Handler);
router.define("Page1", handler: page1Handler);
}
static Handler page2Handler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) => Page2());
static Handler page1Handler = Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) => Page1());
}
class BasePage2 extends StatefulWidget {
#override
State createState() {
return Page2();
}
}
class Page2 extends State<BasePage2> {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Welcome',
home: Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.pink,
child: Icon(Icons.router),
onPressed: () {
Navigator.pushNamed(context, 'Page1');
},
),
appBar: AppBar(
title: Text("Page 2"),
),
body: Center(
child: Text("Page 2"),
)),
);
}
}
//Page 1 looks the same, only the text says "Page 1"
Thank you in advance for any suggestions.
You need to pass the StatefulWidget and not the State:
... => BasePage2()
... => BasePage1()
I am trying to reopen last opened screen after boot, Is there any simple way to do so ? sample codes are welcome !
So far I tried a code(which I got somewhere) with SharedPreferences, but it's not working.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
String lastRouteKey = 'last_route';
void main() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
String lastRoute = preferences.getString(lastRouteKey);
runApp(MyApp(lastRoute));
}
class MyApp extends StatelessWidget {
final String lastRoute;
MyApp(this.lastRoute);
#override
Widget build(BuildContext context) {
bool hasLastRoute = getWidgetByRouteName(lastRoute) != null;
return MaterialApp(
home: Foo(),
initialRoute: hasLastRoute ? lastRoute : '/',
onGenerateRoute: (RouteSettings route) {
persistLastRoute(route.name);
return MaterialPageRoute(
builder: (context) => getWidgetByRouteName(route.name),
);
},
);
}
Widget getWidgetByRouteName(String routeName) {
switch (routeName) {
case '/':
return MainWidget();
case '/':
return SecondRoute();
// Put all your routes here.
default:
return null;
}
}
void persistLastRoute(String routeName) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.setString(lastRouteKey, routeName);
}
}
class Foo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Foo'),
),
body: Column(
children: <Widget>[
RaisedButton(
child: Text('Open route second'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
RaisedButton(
child: Text('Open route main'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => MainWidget()),
);
},
),
],
),
);
}
}
class SecondRoute extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
class MainWidget extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("MainWidget"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
should I use SQLite or JSON instead of SharedPreferences to make the code simple? thanks.
Demo
A. Navigation
when we are navigating through different screens within app, actually, the route stacks are changing.
So, firstly, we need to figure out how to listen to this changes e.g Push screen, Pop back to users screen.
1. Attaching saving method in each action button
we can actually put this on every navigation-related button.
a. on drawer items
ListTile(
title: Text("Beta"),
onTap: () {
saveLastScreen(); // saving to SharedPref here
Navigator.of(context).pushNamed('/beta'); // then push
},
),
b. on Titlebar back buttons
appBar: AppBar(
title: Text("Screen"),
leading: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
saveLastScreen(); // saving to SharedPref here
Navigator.pop(context); // then pop
},
),
),
c. and also capturing event of Phone Back button on Android devices
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: (){ // will triggered as we click back button
saveLastScreen(); // saving to SharedPref here
return Future.value(true);
},
child: Scaffold(
appBar: AppBar(
title: Text("Base Screen"),
),
Therefore, we will have more code and it will be harder to manage.
2. Listening on Route Changes using Route observer
Nonetheless, Flutter provides on MaterialApp, that we can have some "middleware" to capture those changes on route stacks.
We may have this on our MyApp widget :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Save Last Route',
navigatorObservers: <NavigatorObserver>[
MyRouteObserver(), // this will listen all changes
],
routes: {
'/': (context) {
return BaseScreen();
},
'/alpha': (context) {
return ScreenAlpha();
},
We can define MyRouteObserver class as below :
class MyRouteObserver extends RouteObserver {
void saveLastRoute(Route lastRoute) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
prefs.setString('last_route', lastRoute.settings.name);
}
#override
void didPop(Route route, Route previousRoute) {
saveLastRoute(previousRoute); // note : take route name in stacks below
super.didPop(route, previousRoute);
}
#override
void didPush(Route route, Route previousRoute) {
saveLastRoute(route); // note : take new route name that just pushed
super.didPush(route, previousRoute);
}
#override
void didRemove(Route route, Route previousRoute) {
saveLastRoute(route);
super.didRemove(route, previousRoute);
}
#override
void didReplace({Route newRoute, Route oldRoute}) {
saveLastRoute(newRoute);
super.didReplace(newRoute: newRoute, oldRoute: oldRoute);
}
}
B. How to Start the App
As users interacting through the screens, the Shared Preferences will always store last route name. To make the app navigate correspondingly, we need to make our BaseScreen statefull and override its initState method as below :
return MaterialApp(
routes: {
'/': (context) {
return BaseScreen(); // define it as Main Route
},
class BaseScreen extends StatefulWidget {
#override
_BaseScreenState createState() => _BaseScreenState();
}
class _BaseScreenState extends State<BaseScreen> {
#override
void initState() {
super.initState();
navigateToLastPage();
}
void navigateToLastPage() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
String lastRoute = prefs.getString('last_route');
// No need to push to another screen, if the last route was root
if (lastRoute.isNotEmpty && lastRoute != '/') {
Navigator.of(context).pushNamed(lastRoute);
}
}
C. Working Repo
You may look at this repository that overrides RouteObserver as explained in second option above
Saving and Opening Screen Beta and Screen Delta in different starts
D. Shared Preferences / JSON / SQLite
I suggest to use Shared preferences for simplicity. As we only record simple String for route name, we can only write two lines of code to Save and two lines of code to Load.
If we use JSON file, we need to manually set Path for it using path_provider package.
Moreover, if we use SQLite, we need to setup DB (may consist > 8 more lines), and setup table and also inserting table method.