I'm currently trying to push a named route in flutter.
Works good so far, but the Asset Image for the background is loaded after the route was pushed via Navigator, which does not look good.
Currently I push the route like this:
#override
void initState() {
super.initState();
Timer(Duration(seconds: 5), () =>
Navigator.pushNamed(context, routeToPage2)
);
}
Is there any way to load a Page / Route without pushing it in the first place, so everything is build correctly when the route is pushed after the set time?
I guess the Asset Image gets loaded on routeToPage2? In this case the push happens everytime before the image gets load.
you can use a routegenerator for your page route and popUntil. This behavior will remove existing pages off the stack and push a single page called home page on. The MaterialPageRoute builds your route that is pushed to the navigator. Load your asset in the scaffold of HomePage. It should not require a delay to render
Navigator.of(context)
.popUntil(ModalRoute.withName(RouteGenerator.homePage));
class RouteGenerator {
static const String homePage = "/home";
static const String customPage = "/custom";
RouteGenerator._();
static Route<dynamic> handleRoute(RouteSettings routeSettings) {
Widget childWidget;
switch (routeSettings.name) {
case homePage:
{
childWidget = HomePageWidget(title: 'Home Page');
}
break;
case customPage:
{
final args = routeSettings.arguments as CustomView;
childWidget = CustomPageWidget(args);
}
break;
default:
throw FormatException("Route Not Found");
}
return MaterialPageRoute(builder: (context) => childWidget);
}
}
Sorry for being a bit too vague, maybe I should have explained my problem more detailed.
So basically I have Page1, which pushes after a Timer finished to Page2.
On Page2, I have an Image-Asset, which loads shortly after Page2 is displayed, which did not look nice. This asset is saved on the device.
I wanted to load Page2 somehow in the background, while Page1 is still being displayed to the user, so the Background-image of Page2 does not pop up after the Page is shown.
But I have found myself a suitable solution on my problem.
I use the following code on Page1:
late Image backgroundImage;
#override
void initState() {
super.initState();
backgroundImage = Image.asset("path to image");
Timer(Duration(seconds: 5),
() => Navigator.pushReplacementNamed(context, page2));
}
#override
void didChangeDependencies(){
super.didChangeDependencies();
precacheImage(backgroundImage.image, context);
}
This results in preloading the image while Page1 is shown, so the Background-Image does not pop up after Page2 is displayed to the user.
Related
I am working on a Flutter Web App using Getx for navigation and state management. One of the routes in my flutter app has two query parameters. Let us call these parameters Dataset and Index. When the Dataset parameter is changed through the URL, I want to make an API call to retrieve the new dataset, and when the Index parameter is changed, I want to display the data from the dataset at that particular index on the app. Index in this case is an observable RxInt variable defined in the controller.
However, the default behavior when I change the URL and press enter is for the Flutter app to push a new page on to the navigation stack. The behavior I prefer is to simply update the values and make a new API call if necessary. The API call may be done by simply refreshing the page since it is handled by the Getx controller onInit function.
I'm not very familiar with how routing in flutter works and I haven't found a solution to change the behavior for routing itself. I've tried a few ways to update the values despite pushing the new page on to the stack, such as setting the value for index through the initState or build calls on my widgets but those changes aren't visible on my UI. I've also tried reinitializing the controller by deleting it but that didn't work either.
EDIT: I have added a code example:
Widget:
class MainscreenView extends StatefulWidget {
const MainscreenView({Key? key}) : super(key: key);
#override
State<MainscreenView> createState() => _MainscreenViewState();
}
class _MainscreenViewState extends State<MainscreenView> {
late MainscreenController mainscreenController;
#override
Widget build(BuildContext context) {
return GetX<MainscreenController>(
init: MainscreenController(),
initState: (_) {
mainscreenController = Get.find<MainscreenController>();
},
builder: (_) {
return Scaffold(
body: Center(
child: Text(
'Current index is ${mainscreenController.index.value}',
style: const TextStyle(fontSize: 20),
),
),
);
});
}
}
Controller:
class MainscreenController extends GetxController {
final index = 0.obs;
late String? dataset;
#override
void onInit() {
super.onInit();
final String? datasetQuery = Get.parameters['dataset'];
if (datasetQuery != null) {
dataset = datasetQuery; //API call goes here
} else {
throw Exception('Dataset is null');
}
final String? indexString = Get.parameters['index'];
if (indexString == null) {
throw Exception('Index is null');
} else {
final int? indexParsed = int.tryParse(indexString);
if (indexParsed == null) {
throw Exception('Index Cannot be parsed');
} else {
index.value = indexParsed;
}
}
}
}
The initial route is /mainscreen?dataset=datasetA&index=0. If I were to modify the route in the address bar to /mainscreen?dataset=datasetA&index=5 for example and press enter, The current behavior of Flutter is to push a new page onto the navigation stack. I would like to update the value of index instead and display it on the same page, but I haven't found a way to accomplish this. Also, if dataset parameter is updated I would like to again avoid pushing a new page onto the stack and refresh the current page instead so that the onInit function is run again and the API call is made automatically.
I have an app with a BottomNavigationBar and an IndexedStack which shows the tab content. Each tab has its own Router with its own RouterDelegate to mimic iOS-style tab behavior (where each tab has its own navigation controller).
Before, this app was only published on iOS. I'm now working on the Android version and need to correctly support the Android hardware back button. I did this by implementing a ChildBackButtonDispatchers per tab, which are a child of the parent RootBackButtonDispatcher. This works.
The issue I'm having now is that I use WillPopScope widgets to save a user's input when they leave a screen. This works correctly if the user taps the back button in the AppBar, but the callback isn't triggered when the user taps the hardware back button. I implemented BackButtonListeners on these screens as well, but this means I have to wrap the screens in both WillPopScopes and BackButtonListeners, both calling the same callback.
It this how it's supposed to be, or am I doing something wrong?
Relevant widget hierarchy:
MaterialApp
Navigator
tab interface with IndexedStack
the selected tab Widget the tab's Router
Navigator
multiple pages, with on the last page in the stack...
BackButtonListener
WillPopScope
Scaffold
My (simplified) router delegate looks like this:
class AppRouterDelegate extends RouterDelegate<AppRoute>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoute> {
AppRouterDelegate({
List<MaterialPage> initialPages = const [],
}) : _pages = initialPages;
final navigatorKey = GlobalKey<NavigatorState>();
final List<MaterialPage> _pages;
List<MaterialPage> get pages => List.unmodifiable(_pages);
void push(AppRoute route) {
final shouldAddPage = _pages.isEmpty || (_pages.last.arguments as AppRoute != route);
if (!shouldAddPage) {
return;
}
_pages.add(route.page);
notifyListeners();
}
#override
Future<void> setNewRoutePath(AppRoute route) async {
_pages.clear();
_pages.add(route.page);
notifyListeners();
return SynchronousFuture(null);
}
#override
Future<bool> popRoute() {
if (canPop) {
pop();
return SynchronousFuture(true);
}
return SynchronousFuture(false);
}
bool get canPop => _pages.length > 1;
void pop() {
if (canPop) {
_pages.remove(_pages.last);
notifyListeners();
}
}
void popTillRoot() {
while (canPop) {
_pages.remove(_pages.last);
}
notifyListeners();
}
bool _onPopPage(Route<dynamic> route, result) {
final didPop = route.didPop(result);
if (!didPop) {
return false;
}
if (canPop) {
pop();
return true;
} else {
return false;
}
}
#override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _onPopPage,
pages: pages,
);
}
}
I found this Flutter issue which makes me think I shouldn't have the WillPopScope at all, but without it the taps in the AppBar are not caught...
I know this question is old, but here's an answer for others who arrive here.
From the AppBar leading documentation (emphasis mine):
If this is null and automaticallyImplyLeading is set to true, the AppBar will imply an appropriate widget. For example, if the AppBar is in a Scaffold that also has a Drawer, the Scaffold will fill this widget with an IconButton that opens the drawer (using Icons.menu). If there's no Drawer and the parent Navigator can go back, the AppBar will use a BackButton that calls Navigator.maybePop.
So in order to make the Android back button work the same way as the App Bar's back button, you need to use the Navigator.maybePop method, which will respect WillPopScope.
Conveniently, Flutter provides PopNavigatorRouterDelegateMixin to make this easy; it provides an implementation of popRoute that uses maybePop and therefore will work identically to the App Bar's automatically-generated back/dismiss button. The nice thing about Flutter being open source is that you can jump into the Flutter code to verify what the mixin is doing:
mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
/// The key used for retrieving the current navigator.
///
/// When using this mixin, be sure to use this key to create the navigator.
GlobalKey<NavigatorState>? get navigatorKey;
#override
Future<bool> popRoute() {
final NavigatorState? navigator = navigatorKey?.currentState;
if (navigator == null)
return SynchronousFuture<bool>(false);
return navigator.maybePop();
}
}
So I think the only mistake in your code is that, even though you've mixed-in PopNavigatorRouterDelegateMixin on your router delegate, you are also providing your own override of popRoute. When the user taps the Android back button, your popRoute implementation is called, and it just pops the last page. If you delete your popRoute override and let the mixin do its thing, then the Android back button will function identically to the App Bar back/dismiss button.
I have a scenario where i have one popup in screen A .It will be triggerd from initstate() . User can navigate to screen B after clicking on button inside the popup . How can i show the popup if user come back to screen 1 by clicking on arrow back button ?
You can achieve with the help of .then() : https://api.flutter.dev/flutter/dart-async/Future/then.html
Navigator.of(context)
.push(CupertinoPageRoute<Screen1>(
builder: (context) => Screen2(
))).then((value) {
//ShowPopUpMenu() <-- Your PopUpMenu.
});
Add a boolean parameter to your screen if you want the popup:
class PopScreen extends StatefulWidget {
PopScreen (this.displayPopup);
final bool displayPopup;
#override
_PopScreenState createState() => _PopScreenState();
}
class _PopScreenState extends State<PopScreen> {
#override
void initState() {
super.initState();
if(widget.displayPopup){
// display your popup here
}
}
}
call to the navigator:
Navigator.pushNamed(context, '/yourscreen', arguments: true);
So I have a similar issue as the person who asked this older question, except with different requirements that none of the answers there help with.
When a user opens the app, I want them to be greeted with the login page if they haven't logged in or the home page (a bottom nav bar view) if they did. I can define this in the MaterialApp as follows:
MaterialApp(
initialRoute: authProvider.isAuthenticated
? '/home'
: '/login',
routes: {
'/home': (_) =>
ChangeNotifierProvider<BottomNavigationBarProvider>(
child: AppBottomNavigationBar(),
create: (_) => BottomNavigationBarProvider()),
'/login': (_) => LoginView()
},
)
So far so good. Except I want this to work on the web, and now even though the default screen when a user first opens myapp.com is myapp.com/#/login, any user can bypass the login screen by simply accessing myapp.com/#/home.
Now I tried to redirect the user to the login page in the initState() of the bottom navigation bar (and setting the initialRoute to be /home), but on mobile this has undesirable behaviour.
If I try this:
void initState() {
super.initState();
if (!Provider.of<AuthProvider>(context, listen: false).isAuthenticated) {
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pushNamed('/login');
});
}
}
then simply pressing back will return the user to the home page, again bypassing the login. If I try to use popAndPushNamed instead of just pushing, pressing back will open a blank screen (instead of closing the app).
Is there any way to do this correctly so it works on both web and mobile?
If you use the RouteAware mixin on your widget classes, they will be notified when they are navigated to (or away from). You can use this to check if the user is supposed to be there and to navigate them away if they are not:
To use it, first you need some global instance of RouteObserver that all your widget classes can access:
final routeObserver = RouteObserver<PageRoute>();
Then you need to register it with your MaterialApp:
MaterialApp(
routeObservers: [routeObserver],
)
Then register your widget to the route observer:
class HomeViewState extends State<HomeView> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
void didPop() {
// This gets called when this widget gets popped
}
void didPopNext() {
// This gets called when another route gets popped making this widget visible
}
void didPush() {
// This gets called when this widget gets pushed
}
void didPushNext() {
// This gets called with another widget gets pushed making this widget hidden
}
...
}
In your case, you can use the didPush route to navigate the user to the login page if they get to that page in error:
void didPush() {
if (checkLoginStateSomehow() == notLoggedIn) {
Navigator.of(context).pushReplacementNamed('login');
}
}
I have a home page which when clicked takes me to another page through navigates, do some operations in then press the back button which takes me back to the home page. but the problem is the home page doesn't get refreshed.
Is there a way to reload the page when i press the back button and refreshes the home page?
You can trigger the API call when you navigate back to the first page like this pseudo-code
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => new _PageOneState();
}
class _PageOneState extends State<PageOne> {
_getRequests()async{
}
#override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new RaisedButton(onPressed: ()=>
Navigator.of(context).push(new MaterialPageRoute(builder: (_)=>new PageTwo()),)
.then((val)=>val?_getRequests():null),
),
));
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
//somewhere
Navigator.pop(context,true);
}
}
Or you can just use a stream if the API is frequently updated, the new data will be automatically updated inside your ListView
For example with firebase we can do this
stream: FirebaseDatabase.instance.reference().child(
"profiles").onValue
And anytime you change something in the database (from edit profile page for example), it will reflect on your profile page. In this case, this is only possible because I am using onValue which will keep listening for any changes and do the update on your behalf.
(In your 1st page): Use this code to navigate to the 2nd page.
Navigator.pushNamed(context, '/page2').then((_) {
// This block runs when you have returned back to the 1st Page from 2nd.
setState(() {
// Call setState to refresh the page.
});
});
(In your 2nd page): Use this code to return back to the 1st page.
Navigator.pop(context);
use result when you navigate back from nextScreen as follow :
Navigator.of(context).pop('result');
or if you are using Getx
Get.back(result: 'hello');
and to reload previous page use this function :
void _navigateAndRefresh(BuildContext context) async {
final result = await Get.to(()=>NextScreen());//or use default navigation
if(result != null){
model.getEMR(''); // call your own function here to refresh screen
}
}
call this function instead of direct navigation to nextScreen
The solution which I found is simply navigating to the previous page:
In getx:
return WillPopScope(
onWillPop: () {
Get.off(() => const PreviousPage());
return Future.value(true);
},
child: YourChildWidget(),
or if you want to use simple navigation then:
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>PreviousPage() ,));
Simply i use this:
onPressed: () {
Navigator.pop(context,
MaterialPageRoute(builder: (context) => SecondPage()));
},
this to close current page:
Navigator.pop
to navigate previous page:
MaterialPageRoute(builder: (context) => SecondPage())
In FirtsPage, me adding this for refresh on startUpPage:
#override
void initState() {
//refresh the page here
super.initState();
}
For a more fine-grained, page-agnostic solution I came up with this Android Single LiveEvent mimicked behaviour.
I create such field inside Provider class, like:
SingleLiveEvent<int> currentYearConsumable = SingleLiveEvent<int>();
It has a public setter to set value. Public consume lets you read value only once if present (request UI refresh). Call consume where you need (like in build method).
You don't need Provider for it, you can use another solution to pass it.
Implementation:
/// Useful for page to page communication
/// Mimics Android SingleLiveEvent behaviour
/// https://stackoverflow.com/questions/51781176/is-singleliveevent-actually-part-of-the-android-architecture-components-library
class SingleLiveEvent<T> {
late T _value;
bool _consumed = true;
set(T val) {
_value = val;
_consumed = false;
}
T? consume() {
if (_consumed) {
return null;
} else {
_consumed = true;
return _value;
}
}
}
await the navigation and then call the api function.
await Navigator.of(context).pop();
await api call
You can do this with a simple callBack that is invoked when you pop the route. In the below code sample, it is called when you pop the route.
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
_someFunction()async{
Navigator.of(context).push(MaterialPageRoute(builder: (_)=> PageTwo(
onClose():(){
// Call setState here to rebuild this widget
// or some function to refresh data on this page.
}
)));
}
#override
Widget build(BuildContext context) {
return SomeWidget();
}
...
} // end of widget
class PageTwo extends StatelessWidget {
final VoidCallback? onClose;
PageTwo({Key? key, this.onClose}) : super(key: key);
#override
Widget build(BuildContext context) {
return SomeWidget(
onEvent():{
Navigate.of(context).pop();
onClose(); // call this wherever you are popping the route
);
}
}