If I push a route in flutter to a deep part of my app is there any way to supply additional routes so that the back/up navigation can be customized?
You can call Navigator.push() several times in a row; the routes underneath the top one will not visibly transition but they'll be hiding underneath. (Edit: Turns out this isn't true, at least on iOS, see issue 12146)
Note that you can also alter routes below the top route using methods of NavigatorState, such as removeRouteBelow and replaceRouteBelow. This is useful for building non-linear navigation experiences.
I solved this by pushing several routes in a row without animation to solve transition visibility issues. So far, it works fine on iOS for me. Here's a way to do it.
Create a NoAnimationPageRoute by extending MaterialPageRoute and overriding buildTransitions:
class NoAnimationPageRoute<T> extends MaterialPageRoute<T> {
NoAnimationPageRoute({ WidgetBuilder builder }) : super(builder: builder);
#override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
return child;
}
}
Create a function that uses NoAnimationPageRoute:
Future<T> pushWithoutAnimation<T extends Object>(Widget page) {
Route route = NoAnimationPageRoute(builder: (BuildContext context) => page);
return Navigator.push(context, route);
}
Call the function several times in a row:
pushWithoutAnimation(Screen1());
pushWithoutAnimation(Screen2());
pushWithoutAnimation(Screen3());
Related
Is there a way to navigate from one dart "page" to a specific point in another? This will get me to a given page
Navigator.push(
context,
MaterialPageRoute(builder: (context) => WK3()),
);
But I want to navigate to a specific child or row within that page (which are unfortunately fairly long, and would otherwise require a lot of scrolling).
I am used to working with html, where you just have to indicate a position within a page using a hash tag:
#here
That should be possible to do in Flutter/Dart, right?
This is not possible by just using the flutter Navigator. What I would do to tackle that issue is that I would pass an argument which contains the scroll position to the Navigator for example:
Navigator.pushNamed(
context,
'/wk3',
arguments: {'scrollTo': elementId}, // or any other logic like half of the screen or so
);
To read more about Navigator and arguments you can check out the official documentation here. You can also do that for none named routes obviously.
Inside your target widget you could then do the following approach.
Take the argument and parse it to whatever you need.
Depending on your page and your scroll behavior you could use the initState to directly scroll to your desired location. What happens next is a bit dependend on your concrete implementation or where you want to scroll. In certain situations it might be more useful to add a postFrameCallBack for your scrolling instead of doing it in the initState. I'll add it for educational reasons in the snippet below.
Assuming we have a ScrollController of a ListView for example the widget we navigated to knows where we want it to scroll to due to our passed argument. If you use for instance a position value here and we have the ScrollController to do something like this:
controller.position.animateTo(
widget.args.scrollTo, //make sure it has the correct type
duration: const Duration(seconds: 1),
curve: Curves.easeInOut,
);
There are also ways you could scroll to a certain element in a list or a column (like for example the 100th element). Check this question for more information. You can find a slight implentation with a scroll controller below:
class ScreenArguments {
final String scrollTo;
ScreenArguments(this.scrollTo);
}
class Screen extends StatefulWidget {
final ScreenArguments args;
Screen(this.args, {Key key}) : super(key: key);
#override
ScreenState createState() => ScreenState();
}
class ScreenState extends State<Screen> {
#override
void initState() {
scrollMeTo = widget.args.scrollTo;
scrollController = ScrollController();
WidgetsBinding.instance
.addPostFrameCallback((_) => scrollTo(context)); // this is probably safer than doing scrollTo(context) directly in your initState
enter code here
// if you do not use addPostFrameCallback you can call scrollTo(context) directly.
//scrollTo could use scrollControler.animateTo() etc.
}
I dont have ScrollController / ListView implementation
If thats not the case and you do not have a ScrollController and you want just to scroll to any element on your widget things get a little bit more complicated. In that case I'd recommened you to use flutters Scrollable.ensureVisible. Taken from the documentation it does the following:
Scrolls the scrollables that enclose the given context so as to make
the given context visible.
Lets assume you have Column inside a SingleChildScrollView to have a foundation for your scrolling behavior. You would then define a GlobalKey for each section of your widget you would like to scroll to. This key would be the identifier which we pass in as an argument. Assuming we have a GlobalKey in the widget which is called second we could do the following:
Scrollable.ensureVisible(
GlobalObjectKey(widget.args.scrollTo).currentContext, //this would reference second
alignment: 0.5, //
duration: Duration(seconds: 2),
curve: Curves.easeInOut);
You can read more about Scrollable.ensureVisible here.
What approach to take is dependended on your needs and on your implementation.
I've got a stateful widget GameScreen that calls Navigator.pushReplacement to recursively replace itself with another GameScreen instance laid out slightly differently. The pushReplacement causes hero animations to fly elements from one spot to another during the (otherwise invisible) route transition.
I recurse this about 50 times, and all the elements dancing all over the screen look great in theory.
The code is like this (much simplified) example:
final danceProvider = ChangeNotifierProvider<>((ref) => DanceManager(steps: 50));
class GameScreen extends ConsumerStatefulWidget {...}
class GameScreenState extends ConsumerState {
build(context) {
final danceManager = ref.watch(danceProvider);
if (gameIsOver() && danceManager.stepsLeft > 0 && danceManager.ready) {
SchedulerBinding.instance.addPostFrameCallback(
() => _replaceSelfWithNewInstance(danceManager));
}
return ... // structure with layout based on danceProvider.stepsLeft
}
_replaceSelfWithNewInstance(danceManager) {
danceManager.ready = false; // Recurse once per screen
Navigator.pushReplacement(
context,
PageRouteBuilder(
pageBuilder: (context, animation, _) {
animation.addStatusListener(() {
if (animation.status == "complete")
danceManager.ready = true; // Starts next recursion...
});
danceManager.stepsLeft--;
return GameScreen(); // ...in this instance <--
},
),
transitionDuration: Duration(seconds: 1),
);
}
}
It seems the old states aren't being removed from the widget tree until I finally stop calling pushReplacement: at that point they are ALL deactivated and disposed at once. It seems like Navigator is holding onto all the previous screens but not rendering them?
My intent would be that after each screen called pushReplacement to display the next screen in the recursion, it would be out of the tree and would not be rebuilt at all, much less N useless times.
Is there something I might be doing wrong to force Navigator to keep my pushReplaced widgets in the tree, or is this a known thing Navigator does, where we can't be sure when a replaced route will finally be unmounted?
I'm working on a Flutter app and have decided to introduce a splash screen in the same. To navigate to the home screen from the splash screen, normally we use Navigator. But there is one issue that I find in the Navigator method. Whenever i'm popping or pushing a screen, there is a visible navigation, i.e. I can see the screen moving left, right, up or down. What I want to achieve, is that the splash screen should disappear instead of sliding away from the screen. How can I achieve this?
You have to add native splash screen, if you don't want that visible navigation. To add splash screen in your app properly. Please follow the guide: Creating native splash screen
If you are feeling lazy, you can just use the following package : Splash Screen Package. This package is easy to use just read the documentation before installing.
You can use this package flutter_native_splash: ^2.0.5
This package will provide you native splash screen for your apps.
But if you want to disappear your current screen and go to next screen you can use this class custom_route.dart. This class provide you an animation like disappear.
import 'package:flutter/material.dart';
class FadePageRoute<T> extends MaterialPageRoute<T> {
FadePageRoute({
required WidgetBuilder builder,
RouteSettings? settings,
}) : super(
builder: builder,
settings: settings,
);
#override
Duration get transitionDuration => const Duration(milliseconds: 600);
#override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
if (settings.name == "/auth") {
return child;
}
return FadeTransition(
opacity: animation,
child: child,
);
}
}
and finally
onPressed: () {
Navigator.of(context).push(FadePageRoute(
builder: (context) => YourScreen(),
));
},
I'm working on an application and we decided to use the BLoC pattern.
I am facing a recurrent problem in my application.
Indeed, I created a bloc called CatalogBloc.
On my first page, there is a widget that uses the following BlocBuilder:
...
BlocBuilder<CatalogBloc, CatalogState>(
buildWhen: (previous, current) {
return current is CatalogArticlesLoadIsFinished ||
current is CatalogArticlesLoadInProgress;
},
builder: (context, state) {
return CatalogArticlesWidget(
data: state.data,
);
},
);
...
From this page, I can navigate to a page that contains this same BlocBuilder and same widget (CatalogArticlesWidget). This second page calls the bloc CatalogBloc to reload data of the same type, but filtered in initState:
#override
void initState() {
context.read<CatalogBloc>().add(CatalogArticlesLoadRequested(family: widget.family));
super.initState();
}
So when I pop to the first screen (from the second), the data has changed.
What is the cleanest way to avoid this kind of behavior ?
Create a new instance of that bloc for the 2nd page
First solution: see w461 answer.
Second solution: in my case, I think it is better to create new states for each page.
Currently, I can submit edits to a single page in a PageView and then either Navigator.push to a newly created single edited page or Navigator.pop back to the original Pageview containing the unedited page.
But I'd prefer to pop back to the the same place in an updated/refreshed Pageview. I was thinking I could do this on the original PageView page:
Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
But after editing, how can I pop back to a refreshed PageView which is scrolled to the now updated original page? Or is there a better way? Someone mentioned keys, but I've not yet learned to use them.
The question deals with the concept of Reactive App-State. The correct way to handle this is through having an app state management solution like Bloc or Redux.
Explanation: The app state takes care of the data which you are editing. the EditPage just tells the store(App-State container) to edit that data and the framework takes care of the data that should be updated in the PageView.
as a temporary solution you can use an async call to Navigation.push() and refresh the PageView State once the EditPage comes back. you can also use an overloaded version of pop() to return a success condition which aids for a conditional setState().
Do you know that Navigator.pushReplacement(...) returns a Future<T> which completes when you finally return to original context ?
So how are you going to utilize this fact ?
Lets say you want to update a String of the original page :
String itWillBeUpdated="old value";
#override
Widget build(BuildContext ctx)
{
.
.
.
onPressesed:() async {
itWillBeUpdated= await Navigator.pushReplacement(context,new MaterialPageRoute(
builder: (BuildContext context) => EditPage()),);
setState((){});
},
}
On your editing page , you can define Navigator.pop(...) like this :
Navigator.pop<String>(context, "new string");
by doing this , you can provide any data back to the original page and by calling setState((){}) , your page will reflect the changes
This isn't ideal, but works somewhat. First I created a provider class and added the following;
class AudioWidgetProvider with ChangeNotifier {
int refreshIndex;
setRefreshIndex (ri) {
refreshIndex = ri;
return refreshIndex;
}
}
Then in my PageView Builder on the first page, I did this;
Widget build(context) {
var audioWidgetProvider = Provider.of<AudioWidgetProvider>(context);
return
PreloadPageView.builder(
controller: PreloadPageController(initialPage: audioWidgetProvider.refreshIndex?? 0),
Then to get to the EditPage (2nd screen) I did this;
onPressed: () async {
audioWidgetProvider.setRefreshIndex(currentIndex);
Navigator.pushReplacement(context,new MaterialPageRoute(builder: (BuildContext context) => EditPage()),); }
And finally I did this to return to a reloaded PageView scrolled to the edited page;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) =>HomePage()));
The only problem now is that the PageView list comes from a PHP/Mysql query and I'm not sure what to do if new items are added to the list from the Mysql database. This means the currentIndex will be wrong. But I guess that's the topic of another question.