Something like conditional routing in Flutter? - flutter

Consider I have 3 screens namely screen 1, screen 2, screen 3 and screen 4.
I want to achieve the following.
Screen 3 is opened by Screen 1 -> BackButton -> Screen 2
Screen 3 is opened by Screen 2 -> BackButton -> Screen 3
Screen 3 is opened by Screen 4 -> BackButton -> Screen 1
Moreover, iOS automatically sets a swipe back option. I want to overwrite it that a swipe back in iOS does the same as described above.
Is there something like conditional routing in Flutter which helps me to adjust the BackButton-behaviour in accordance to 'from which Screen was my current Screen opened (navigator.push)'?

Wrap your widget tree in a WillPopScope() widget. This widget has an onWillPop property that you can override to whatever you want - in this case, depending on the screen you're on you'll probably want to override it to
onWillPop: () => Navigator.pushReplacement(<correctScreenWidget>)
This should catch any attempts to go back and instead do whatever you override it to. Be sparing with it, overriding default back button behaviour can make for a weird user experience if done poorly.
As for the conditional part of it, unfortunately it's a bit tricky as Navigator._history is private, so we can't just check the previous route that way. Best bet is to set up a NavigatorObserver to keep track of previous routes, and set the name in the RouteSettings of each of your routes to keep track.
Step one is to create an observer and provide it to your Navigator, something like this:
class PreviousRouteObserver extends NavigatorObserver {
Route _previousRoute;
Route get previousRoute => _previousRoute;
String get previousRouteName => _previousRoute.settings.name;
#override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
_previousRoute = previousRoute;
}
#override
void didReplace({Route<dynamic> newRoute, Route<dynamic> oldRoute}) {
_previousRoute = oldRoute;
}
}
class MyApp extends StatelessWidget {
final PreviousRouteObserver observer = PreviousRouteObserver();
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page', observer: observer),
navigatorObservers: [
observer,
],
);
}
}
Note that MyHomePage above needs to accept the observer as an argument so you can access it. Alternatively you could set up an InheritedWidget or something to maintain access to it, but this answer is getting a little long already so I'll leave that for a later question.
Then, when providing Routes to your Navigator, ensure you've got a name in the RouteSettings:
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => NextScreen(observer: widget.observer),
settings: RouteSettings(name: "nextScreen"),
),
);
Finally, do conditional routing based on the current value of widget.observer.previousRouteName in any widget that has access to it. This is just a simple matter of a switch or something in your onWillPop, so I'll leave that to you.
Kind of unfortunate that it's so roundabout, but it looks like this might be your best option at the moment. Hope it helps!

Related

How can you access GoRoute state.params outside of the routing process?

I hope some simple pseudocode is enough so that both me and you can understand the question and answer.
The problem I'm facing is especially hard when using flutter-web, where the refresh restarts the whole program.
I want to use the path parameters to build objects in a child widgets build method.
GoRouter(routes:...,GoRoute(path="/categories/:category/",
builder:(context,state){
category = state.params['category];
return ParentWidget(category);}
ParentWidget extends StatelessWidget {
build(context){
return ChildWidget();}}
ChildWidget extends StatlessWidget {
build(context){
return "do something with category";}}
Now one way which I can think of and should technically work without any errors would be to pass the params first into the ParentWidget and then pass it along to the next child and so on. But if there's a long chain of child widgets it gets quite tedious and I'm guessing error prone as well. The other thing I was thinking was to use providers: pass the param once again to the parent widget and then make the parent widget send it to a provider. But then the question becomes, where do I do it? Apparently I shouldn't update a provider on build(), but if I do it on initState() it only does it bugs out if I change into a route that include the same widget tree but different path e.g. /categories/apples -> /categories/bananas.
Ps. For some reason I don't remember what the problem with refreshing was. (It has something to do with resetting the providers). But I'll update it when I remember.
go_router has it's params in its state.
Hence pass the state to the page
Router
GoRoute(
name: "test",
path: "/test/:id",
builder: (context, state) {
return SampleWidget(
goRouterState: state, 👈 Pass state here
);
},
),
Usage
context.goNamed("test", params: {"id": "123"}),
Accesing in the page
class SampleWidget extends StatelessWidget {
GoRouterState? goRouterState;
SampleWidget({super.key, this.goRouterState});
#override
Widget build(BuildContext context) {
print(goRouterState?.params.toString()); 👈 access anywhere like so
return const Scaffold(
body: ...
);
}
}

Flutter Navigator.pushReplacementNamed show back button at the page

I'm using Navigator.pushReplacementNamed in flutter app to navigate from loginpage to homepage working well, but in my homepage show back arrow button at the appBar and it returns to loginpage when pressing it. What should I do? I tried put leading: Text(''), but when pressing physical back button it still goes back to loginpage.
I have a logout button and I want user to logout only by this button, not from back button
*this is my first question here, sorry for my poor English or any mistakes
You should use this instead:
.pushNamedAndRemoveUntil(/* Your Route */, (Route<dynamic> route) => false)
As I understand your UX flow, the user will always be directed to the login page on the app start. If the user is already logged in, you should avoid navigating to this (at that moment useless) route. Instead try to distinguish in the build method if the user is logged in or not. If the user is already logged in, build the homepage. If the user is not logged in, build the login page. As soon as the user logs in, the state changes and the homepage will be build.
// your stateful component
bool isLoggedIn;
#override
Widget build(BuildContext context) {
if(isLoggedIn) {
return _buildHomepage();
} else {
return _buildLoginPage();
}
}
Widget _buildHomepage() {
// build your homepage
}
Widget _buildLoginPage() {
// build your login page
}
I had this problem today and came across your Post.
While PushNamedAndRemoveUntil works, the solution in my case was even more simple:
Make sure you're not Naming a route, which is not your Homescreen '/'.
Something like:
MaterialApp(
title: 'Flutter Demo',
initialRoute: Screen1.routeName, // routeName = "/route1"
routes: {
Screen1.routeName: (ctx) => Screen1(), // routeName = "/route1"
Screen2.routeName: (ctx) => Screen2(), // routeName = "/"
Screen3.routeName: (ctx) => Screen3(), // routeName = "/screen2"
},
);
})
Will start your App with Screen1, but place Screen2 at the top of the Navigator's Stack.

Updated values get overridden on Widget redraw

I am playing around with a simple flutter countdown app. It consists of 2 pages, the clock and a settings page to set minutes and seconds to be counted down.
On the clock page (HomeWidget) the user clicks a button to navigate to the settings page. After editing the values the user presses the back hardware key or the button in the app bar to navigate back to the clock page.
class _HomeWidgetState extends State<HomeWidget> {
#override
Widget build(BuildContext context) {
TimeService _timeService = ScopedModel.of<TimeService>(context);
SettingsModel _settingsModel = ScopedModel.of<SettingsModel>(context);
_timeService.setTime(_settingsModel.minutes, _settingsModel.seconds);
return Scaffold( ... display the clock, navigation buttons, etc ... )}
My problem to understand is that when navigating back I am setting the new values in the time service class that handles counting down. But in the code sample the time service is updated every time the clock gets redrawn (every second). The countdown doesn't work, the value remains the same. Instead of displaying "10:29", it sticks with "10:30". I don't know how to handle the dependency between my TimeService class and my SettingsModel class.
How can I handle the assignment of the settings values in the time service class properly when the user navigates back? The build method is obviously the wrong place. Can anyone give me a hint?
Ok, I found a solution for my problem. It is described in detail (with some other content) here.
Basically when navigating between pages you can pass objects along. So now I just pass the edited SettingsModel on the settings page via a Navigator.of(context).pop({'newSetting': _settingsModel}); command and the clock page then handles the result. I wasn't aware that navigation works like this.
ControlButtonWidget(
icon: Icons.settings,
iconSize: 72.0,
onPressedHandler: () async {
Map results = await Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) => SettingsWidget(model)));
if (results != null && results.containsKey("newSetting")){
SettingsModel model = results["newSetting"];
ScopedModel.of<TimeService>(context).setTime(model.minutes, model.seconds);
}
})
Make sure to wrap your page in a WillPopScope Widget.
#override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: _backRequestedHandler,
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints viewportConstraints) {
return Scaffold(
appBar: new AppBar(title: Text("Settings")),
body: ...
}));
}
Future<bool> _backRequestedHandler() {
Navigator.of(context).pop({'newSetting': _settingsModel});
return new Future.value(true);
}

Why is Flutter disposing my widget state object in a tabbed interface?

I have a tabbed Flutter interfaces using DefaultTabController with 3 pages, each a stateful widget. I seem to be able to switch between the first two tabs just fine, but when I tab to the 3rd page the state object for the first page gets disposed. Subsequent state updates (using setState()) then fail.
I've overridden the dispose() method of the state object for the first page so that it prints a message when disposed. It's getting disposed as soon as I hit the third tab. I can't find documentation on why Flutter disposes state objects. (Plenty on lifecycle but not the reasons for progressing through the stages.)
Nothing unusual about setting up the tabs, I don't think.
#override
Widget build(BuildContext context) {
return new MaterialApp(
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: Text(title),
bottom: TabBar(
tabs: [
// Use these 3 icons on the tab bar.
Tab(icon: Icon(Icons.tune)),
Tab(icon: Icon(Icons.access_time)),
Tab(icon: Icon(Icons.settings)),
],
),
),
body: TabBarView(
children: [
// These are the 3 pages relating to to the tabs.
ToolPage(),
TimerPage(),
SettingsPage(),
],
The pages themselves are pretty simple. No animation. Just switches and sliders, etc. I think about the only departures I've made from the code examples I've seen are that I've made the main app widget stateful, I've extended the tab pages to support wantKeepAlive() and I override wantKeepAlive to set it to true (thought that might help), and I call setState() on the first two tabs from an external object, which Android Studio flags with a weak warning. State on the first two pages gets updated when reading from a websocket this app opens to a remote server. Upon further testing I've noticed it only happens only when I tab from the first to the third page. Going from first to second or second to third does not trigger the dispose.
I would expect the State object associated with a StatefulWidget to stay around and with wantKeepAlive = true don't understand why it's being disposed when I click to the third tab, especially because it doesn't happen when I click to the second or from the second to third.
This happens because TabBarView doesn't always show all tabs. Some may be outside of the screen boundaries.
In that case, the default behavior is that Flutter will unmount these widgets to optimize resources.
This behavior can be changed using what Flutter calls "keep alive".
The easiest way is to use AutomaticKeepAliveClientMixin mixin on a State subclass contained inside your tab as such:
class Foo extends StatefulWidget {
#override
_FooState createState() => _FooState();
}
class _FooState extends State<Foo> with AutomaticKeepAliveClientMixin {
#override
bool wantKeepAlive = true;
#override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
}
This tells Flutter that Foo should not be disposed when it leaves the screen.

Flutter Pageview : swiping backward calls build method on widgets in unexpected ways

Consider this simple code snippet, a snippet I wrote to reproduce my problem. It is just a pageview with 3 stateful widgets into it. (It is stand alone, you can copy-paste it and run it):
import 'package:flutter/material.dart';
void main() => runApp(new TestInherited());
class TestInherited extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Pageview test',
home: PageView(
children: <Widget>[TestWidget("1"), TestWidget("2"), TestWidget("3")],
));
}
}
class TestWidget extends StatefulWidget {
final String _name;
String get name => _name;
#override
TestWidgetState createState() {
return TestWidgetState();
}
TestWidget(this._name);
}
class TestWidgetState extends State<StatefulWidget> {
#override
Widget build(BuildContext context) {
TestWidget w = widget;
print("building ${w._name}");
return Center(child: Text(w._name));
}
}
When swiping forward from page 1 to 2 to 3, you see the build methods being called nicely (due to the print statements).
But when going backward, from page 3 to page 2 this does not happen for page 2. But going from 2 to 1 it is called for page 1.
So:
3 to 2: nothing happens
2 to 1: prints "building 1"
That is a problem for me since in my project on page 3 I change stuff that influences page 2 so it needs to rebuild.
It even gets more strange when just adding a few more pages (say we have 5). Then going forward works fine, (the print happens on every page) but going backward...
5 to 4: prints "building 3" (??)
4 to 3: nothing
3 to 2: prints "building 2"
2 to 1: prints "building 1"
Can anyone explain this to me, please?
Thanks a lot for your time and help!
As a general rule, you should assume that nothing will be rebuilt if you want it to be rebuilt and everything will be rebuilt if you want it not to be. The model is that we rebuild everything every frame, but we have optimizations to remove rebuilds that we can prove aren't necessary.
In this case, since you never call setState, we don't really need to rebuild anything. We probably only rebuild the widgets that we discarded to save memory when they're not visible.
The TestWidget should call setState when it needs to be rebuilt. You could have page 3 send a notification to page 2 via a Listenable or some such and have page 2 use that to know when to rebuild.