Navigator not removing old widget from tree after pushReplacament() - flutter

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?

Related

Is there a way to navigate to an specific child or row in flutter?

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.

How to close all overlays in flutter

I have built a screen that builds an overlay which contains a search bar widget.
Because I've built the overlay call in one class which then loads a separate widget class containing the search bar. I can't workout how to close the overlay from the active search bar class.
I have tried creating a close or dispose function but this only works when called from the parent class that initiated the overlay and not the child class containing the search widget.
Is there a close or kill all overlay function that I can use in this situation?
Or a way of calling the dispose / close overlay function that resides in the parent class from the child class when the search widget is running?
The only work-around I can think of is to rebuild the overlay and the search bar functionality into a single class which seems wrong.
I had the same issue and here is my solution. I call the close function prior to opening a new overlay:
// OVERLAYS
OverlayEntry? overlayWidget;
void closeOverlay() {
if ( overlayWidget == null || overlayWidget?.mounted == false) return;
overlayWidget?.remove();
overlayWidget = null;
}
void overlayWidgetDialog(BuildContext context, {required Widget child}) {
// avoid stacking
closeOverlay();
overlayWidget = OverlayEntry(
// maintainState: true,
// opaque: true,
builder: (context) {
return child;
});
Overlay.of(context)?.insert(overlayWidget!);
}
If you look at the code for Overlay you'll see this gem:
Do you see the highlighted part? Internally Flutter uses a private variable called "_entries" that has access to all overlays that are displayed at the moment.
However this is a private variable. So to answer your question, unless you keep a reference to all your overlay entries and then closing them manually, you cannot ask the system to do that for you.

Flutter paint(): is this legit? Rendering Renderobject in different context

I try to render the child of a listitem (somewhere up) to a different place (widget) in the tree;
In the approach below, BlendMask is the "target" widget that checks for and paints "source" widgets that got themself an GlobalKey which is stored in blendKeys.
This works somewhat. And I'm not quite sure if I might fight the framework, or just missing some points...
The problems are two:
The minor one: This approach doesn't play nice with the debugger. It compiles and runs fine but every hot-reload (on save f.e.) throws an "can't findRenderObject() of inactive element". Maybe I miss some debug flag?
the real problem, that brought me here questioning the idea en gros: As mentioned, the Source-Widget is somewhere in the subtree of the child of a Scrollable (from a ListView.build f.e.): How can I update the Òffset for the srcChild.paint() when the list is scrolled? - without accessing the lists scrolController?! I tried listening via WidgetsBindingObservers didChangeMetrics on the state of the Source widget, but as feared no update on scroll. Maybe a strategically set RepaintBounderyis all it needs? *hope* :D
Anyway, every tip much appreciated. Btw the is an extend of this question which itself extends this...
class BlendMask extends SingleChildRenderObjectWidget {
[...]
#override
RenderObject createRenderObject(context) {
return RenderBlendMask();
}
}
class RenderBlendMask extends RenderProxyBox {
[...]
#override
void paint(PaintingContext context, offset) { <-- the target where we want to render a widget
[...] from somewhere else in the tree!
for (GlobalKey key in blendKeys) {
if (key.currentContext != null) {
RenderObject? srcChild <-- the source we want to render in this sibling widget!
= key.currentContext!.findRenderObject();
if (srcChild != null) {
Matrix4 mOffset = srcChild.getTransformTo(null);
context.pushTransform(true, offset, mOffset, (context, offset) {
srcChild.paint(context, offset);
});
}
}
}
}
} //RenderBlendMask

Flutter/Dart - Edit Page in PageView, then Refresh and Scroll back to Same Place?

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.

How to push multiple routes with Flutter Navigator

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());