Proper route guarding implementation with auto_route - flutter

I am trying to understand how guarding works, my setup is as follows:
router
#AdaptiveAutoRouter(
replaceInRouteName: 'Page,Route',
routes: <AutoRoute>[
AutoRoute(
page: LoginPage,
initial: true,
path: '/login',
),
AutoRoute(page: HomePage, path: '/home', guards: [AuthGuard]),
],
)
class $AppRouter {}
guard
class AuthGuard extends AutoRouteGuard {
//from context.watch<AuthService>().isAuthenticated
AuthGuard({required this.isAuthenticated});
final bool isAuthenticated;
#override
void onNavigation(NavigationResolver resolver, StackRouter router) {
if (isAuthenticated) {
resolver.next(isAuthenticated);
} else {
router.push(LoginRoute());
router.popForced();
// resolver.next();
}
}
}
service
class AuthService extends ChangeNotifier {
bool isAuthenticated = false;
login() {
isAuthenticated = true;
notifyListeners();
}
logout() {
isAuthenticated = false;
notifyListeners();
}
}
screens
class LoginPage extends StatelessWidget {
const LoginPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Login Page'),
),
body: Center(
child: Column(
children: [
ElevatedButton(
onPressed: () {
context.read<AuthService>().login();
context.pushRoute(HomeRoute());
},
child: Text('Authenticate Me')),
ElevatedButton(
onPressed: () {
context.pushRoute(HomeRoute());
},
child: Text('Go Home')),
],
),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
context.read<AuthService>().logout();
},
child: Text('Uauthenticate Me')),
),
);
}
}
Now clicking Go Home button prevents me from navigating to the home page which is correct, however when I click Authenticate Me button, it does not route me to the HomeRoute but instead I get a blank screen while the path still shows /login.

Sorry I don't see where you pass your argument isAuthenticated, maybe this is the issue ? I've seen you gave up auto_route in the comment, I post this answer for you or those who face issue to handle guard on provider.
Provider
You can get the context to get your provider from your guard.
#override
void onNavigation(NavigationResolver resolver, StackRouter router) async {
final context = router.navigatorKey.currentContext;
context.read< AuthService>()
// Use your provider
}

Related

How to change the value and the function of a button on flutter?

I have a function named saveData which is executed on pressing a button. I want if I click on the button I execute saveData function and the value of the button become stop then when I click on stop the function should be fininish.
this is the button code:
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: () {
saveData();
},
child: Text('Save Data'),
),
),
One way to achieve what you want is simply to create a flag to control which button (text/action) is shown at any given moment:
TextButton(
onPressed: isSaving ? Finish : saveData,
child: isSaving ? const Text("Stop") : const Text("Save Data"),
)
Try the following working complete sample to see what i mean:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool isSaving = false;
Future saveData() async {
isSaving = true;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Saving data..."),duration: Duration(hours: 1),)
);
setState(() { });
}
void Finish() {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Saving data stopped..."),duration: Duration(seconds: 1),)
);
isSaving = false;
setState(() { });
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: TextButton(
onPressed: isSaving ? Finish : saveData,
child: isSaving ? const Text("Stop") : const Text("Save Data"),
)
),
);
}
}
This will produce a result like:
State 1
After Save Data is tapped
You need state management.
State Management
This is a way to manage your user interface controls such as text fields, buttons, images, etc. It controls what and when something should display or perform an action. More about Flutter state management here
Codebase Sample
String name = ""; // setting an empty name variable
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: () {
setState(() {
name = "new name"; // updating the name variable with setState
});
},
child: Text('Save Data'),
),
),
Now, to implement your idea. You need a bool variable that changes the state on the button click action. To do that, look what I did below
bool isClicked = false;
Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: () {
setState(() => isClicked = !isClicked); // change click state
if (isClicked) {
// do something on click
} else {
// do something off click
}
},
child: Text(isClicked ? "Stop" : "Save Data"), // if isClicked display "Stop" else display "Save Data"
),
),
Another way to do this is to create two different functions. One for saving user data, and the other of stop and calling the action based on a bool state.
onPressed: isSaving ? saveData : stop,
You can use the method above to update your user data as well if any misunderstand or need future help, comment below. Bye
Basically this is a state management problem.
you get more information about state management from here
Here a code for solve your problem
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const HomeView(),
);
}
}
class HomeView extends StatefulWidget {
const HomeView({Key? key}) : super(key: key);
#override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
bool _savePressed = false;
void _save() {
// TODO do whatever you want
}
void _stop() {
// TODO do whatever you want
}
void _onButtonPressed() {
setState(() {
_savePressed = !_savePressed;
_savePressed ? _save() : _stop();
});
}
String get _getButtonText => _savePressed ? "Stop" : "Save";
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Align(
alignment: Alignment.bottomCenter,
child: TextButton(
onPressed: _onButtonPressed,
child: Text(_getButtonText),
),
),
);
}
}

Make a transition when reordering pages in Navigator.pages

I have a list of pages in Navigator.pages. When I push a page with a duplicate key, I want to bring the old page on top. The problem is, the page shows without animation, it just appears on top instantly. Meanwhile the default animation works alright when pushing a new page for the first time. How do I animate the page if it is brought on top?
Here is the code:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
final _notifier = ChangeNotifier();
final _pages = <Page>[Page1()];
class Page1 extends MaterialPage {
Page1()
: super(
key: const ValueKey('Page1'),
child: Scaffold(
appBar: AppBar(title: const Text('Page1')),
body: const Center(
child: ElevatedButton(
onPressed: _showPage2,
child: Text('Show Page2'),
),
),
),
);
}
class Page2 extends MaterialPage {
Page2()
: super(
key: const ValueKey('Page2'),
child: Scaffold(
appBar: AppBar(title: const Text('Page2')),
body: const Center(
child: ElevatedButton(
onPressed: _showPage1,
child: Text('Show Page1'),
),
),
),
);
}
void _showPage1() {
for (final page in _pages) {
if (page.key == const ValueKey('Page1')) {
_pages.remove(page);
break;
}
}
_pages.add(Page1());
_notifier.notifyListeners();
}
void _showPage2() {
for (final page in _pages) {
if (page.key == const ValueKey('Page2')) {
_pages.remove(page);
break;
}
}
_pages.add(Page2());
_notifier.notifyListeners();
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _notifier,
builder: (_, __) {
return Navigator(
pages: [..._pages],
transitionDelegate: const MyTransitionDelegate(),
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
return true;
},
);
},
);
}
}
What I tried:
Navigator.transitionDelegate seemed like the property to handle this. But it is only invoked when a new route is pushed and not when the order is changed.
To dispose the old route before bringing the page on top in hope that it will be re-created by Flutter and that could somehow trigger the animation. But I just get an exception saying the route cannot be used after disposal.
To let the navigator rebuild without the old page with _changeNotifier.notifyListeners(); await Future.delayed(Duration.zero); before re-adding the page. But the zero duration did not work.
The workarounds so far are:
To add a non-zero duration between removing and re-adding a page. But this is noticeable.
To change the key of the new page. But I need keys to track some things in the app.

Nested routes in beamer with partial page load [Flutter]

I'm using Beamer and Flutter to build an app
I've built logic for the default landing page of the application like a landing screen or a marketing page, then, once someone is logged in, a bottom navigator appears, I want this bottom nav to change the output of the middle of the screen (the Scaffold.body) but not refresh the browser or reload the bottom navbar itself, but I want the url to change
The routes will look something like this
- localhost
|-- /
|-- /login
|-- /dashboard
|-- /
|-- /account
|-- /history
There is example code for a nested router on the beamer repo that does what I want, with only a partial page update with animation but I can't figure out how the code works, i've been reading it for a few hours and I've made no progress
Here is a minimal reproduction of where I am stuck
import 'package:community_material_icon/community_material_icon.dart';
import 'package:flutter/material.dart';
import 'package:url_strategy/url_strategy.dart';
import 'package:beamer/beamer.dart';
void main() {
setPathUrlStrategy();
runApp(const MyApp());
}
class DefaultPage extends StatelessWidget {
const DefaultPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('app_title home page')),
body: Column(
children: [
const Text("Welcome to the app"),
TextButton(
onPressed: () {
Beamer.of(context).beamToNamed('/login');
},
child: const Text("Go to login page"),
),
],
),
);
}
}
class LoginPage extends StatelessWidget {
const LoginPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('app_title login page')),
body: Column(
children: [
const Text("Press button to login"),
TextButton(
onPressed: () {
Beamer.of(context).beamToNamed('/dashboard');
},
child: const Text("Login"),
),
],
),
);
}
}
class DashboardPage extends StatefulWidget {
const DashboardPage({Key? key}) : super(key: key);
#override
State<StatefulWidget> createState() => _DashboardPageState();
}
class _DashboardPageState extends State<DashboardPage> {
final _beamerKey = GlobalKey<BeamerState>();
var _currentIndex = 0;
void _onTapUpdateIndex(index) {
setState(() {
_currentIndex = index;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('app_title dashboard')),
body: Beamer(
key: _beamerKey,
routerDelegate: BeamerDelegate(
transitionDelegate: const NoAnimationTransitionDelegate(),
initialPath: '/dashboard',
locationBuilder: RoutesLocationBuilder(
routes: {
'/': (context, state, data) => const Text('Home'),
'/account': (context, state, data) => const Text('Account'),
},
),
),
),
bottomNavigationBar: BottomNavigationBar(
type: BottomNavigationBarType.fixed,
currentIndex: _currentIndex,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(CommunityMaterialIcons.account), label: 'Account'),
],
onTap: (index) {
var pages = ['/dashboard', '/dashboard/account'];
_onTapUpdateIndex(index);
context.beamToNamed(pages[index]);
},
),
);
}
}
// The default router delegate handles logins, splash screen landings and redirects to the dashboard
// Once at the dashboard I want a new router to check all /dashboard/:page requests and update only partial amounts of the page
final _routerDelegate = BeamerDelegate(
initialPath: '/',
locationBuilder: RoutesLocationBuilder(
routes: {
'/': (context, state, data) => const DefaultPage(),
'/login': (context, state, data) => const LoginPage(),
'/dashboard': (context, state, data) => const DashboardPage(),
},
),
);
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'app_title',
theme: ThemeData(),
routerDelegate: _routerDelegate,
routeInformationParser: BeamerParser(),
backButtonDispatcher: BeamerBackButtonDispatcher(delegate: _routerDelegate),
);
}
}
Currently the only way I've made any progress is by defining all of the routes in the main router and having no nested routing functionality then anytime someone clicks a new link the whole dashboard is reloaded

How to Refresh State from Navigator Pop in Flutter

I want to refresh the state when calling Navigator Pop / Navigator Pop Until.
While I was doing some research, I finally found this article Flutter: Refresh on Navigator pop or go back. From the code in the article, it can work fine.
But there is a problem when I use the widget tree, for example like the code below:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Refresh on Go Back',
home: HomePage(),
);
}
}
Home Page - Parent Class
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int id = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Data: $id',
style: Theme.of(context).textTheme.headline5,
),
ButtonWidget(),
],
),
),
);
}
void refreshData() {
id++;
}
onGoBack(dynamic value) {
refreshData();
setState(() {});
}
}
Button Widget - Widget Class
class ButtonWidget extends StatelessWidget{
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: (){
Navigator.push(context, MaterialPageRoute(builder: (context) =>
SecondPage())).then(onGoBack);
// The Problem is Here
// How to call a Method onGoBack from HomePage Class
}
);
}
}
SecondPage
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
),
);
}
}
Or is there another solution to refresh the state class when calling Navigator Pop / Navigator Pop Until?
re-write your Button's class like this:
class ButtonWidget extends StatelessWidget{
final Function onGoBack;
ButtonWidget({this.onGoBack})
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: (){
Navigator.push(context, MaterialPageRoute(builder: (context) =>
SecondPage())).then(onGoBack);
//to avoid any np exception you can do this: .then(onGoBack ?? () => {})
// The Problem is Here
// How to call a Method onGoBack from HomePage Class
}
);
}
}
And add the onGoBack function as a parameter from the home page like this:
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int id = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Data: $id',
style: Theme.of(context).textTheme.headline5,
),
ButtonWidget(onGoBack: onGoBack),
],
),
),
);
}
void refreshData() {
id++;
}
onGoBack(dynamic value) {
refreshData();
setState(() {});
}
}
you must sent function on widget
class ButtonWidget extends StatelessWidget{
final Function(dynamic)? refresh;
const ButtonWidget({this.refresh})
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: ()async {
await Navigator.push(context, MaterialPageRoute(builder: (context) =>
SecondPage()));
if(refresh!=null){
refresh!("your params");
}
// The Problem is Here
// How to call a Method onGoBack from HomePage Class
}
);
}
}
and you can use widget
ButtonWidget(
refresh:onGoBack
)
Try this, it just you are calling method out of scope
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Refresh on Go Back',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int id = 0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Data: $id',
style: Theme.of(context).textTheme.headline5,
),
ButtonWidget(
refresh: onGoBack,
)
],
),
),
);
}
void refreshData() {
id++;
}
onGoBack(dynamic value) {
refreshData();
setState(() {});
}
}
class ButtonWidget extends StatelessWidget {
final Function(dynamic)? refresh;
ButtonWidget({Key? key, this.refresh}) : super(key: key);
#override
Widget build(BuildContext context) {
print(refresh);
return RaisedButton(onPressed: () async {
await Navigator.push(
context, MaterialPageRoute(builder: (context) => SecondPage()))
.then((value) => refresh!("okay"));
});
}
}
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Second Page'),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go Back'),
),
),
);
}
}

How do I initialize data with the Provider in flutter

This is code:
main
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider<Something>(
create: (_) => Something(),
child: Consumer<Something>(
builder: (BuildContext context, Something value, Widget child) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
},
),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String mockData = '';
#override
void initState() {
super.initState();
initData();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('demo'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'moceData:$mockData',
),
Text(
'${Provider.of<Something>(context).count}',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return SecondPage();
}));
},
child: Icon(Icons.add),
),
);
}
initData() {
Future.delayed(Duration(seconds: 1), () {
mockData = 'mock 123';
setState(() {});
});
}
}
SecondPage
class SecondPage extends StatefulWidget {
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends State<SecondPage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
child: Center(
child: GestureDetector(
onTap: () {
Provider.of<Something>(context, listen: false).doSomething();
},
child: Text('click'),
),
),
),
);
}
}
Something
class Something extends ChangeNotifier {
var count =0;
void doSomething() {
print('doSomething');
count++;
notifyListeners();
}
}
when we open this app, MyHomePage request data in initState,
when we push secondPage,we click ‘click’ btn,We want the first page to retrieve the data(iniData()).
when we click ,notifiyListeners() and _MyHomePageState build()is called, but initState()is not,so
how to do?we can invoke initData again.
Similar situation:
1.We have changed the language on other pages. The data on the home page needs to re-request the language interface of the response.
2.After the user logs in successfully, refresh the user inventory, the inventory page already exists
Try this :
setState(() {
mockData = 'mock 123';
});
But here you are not initializing data to use it with Provider, if you are looking to get data ( i mean mockData var ) with Provider , you can do that :
in Something class you add this:
String mockData="123";
String get mockdata => mockData;
and then in the HomePage you access this data using the Provider :
Provider.of<Something>(context, listen:false).mockdata;
i hope i could help you.. good luck !
sorry,Maybe I didn't describe the problem clearly enough, but I have found a solution now.
use
ChangeNotifierProxyProvider<Foo, MyChangeNotifier>(
create: (_) => MyChangeNotifier(),
update: (_, foo, myNotifier) => myNotifier
..foo = foo,
child: ...
);
/// A [ChangeNotifierProvider] that builds and synchronizes a [ChangeNotifier]
/// from values obtained from other providers.
Thanks