My goal is to track routes being presented and dismissed for analytics purposes like described on this article: https://medium.com/flutter-community/how-to-track-screen-transitions-in-flutter-with-routeobserver-733984a90dea
I am pushing a CupertinoPageRoute and passing a Widget to it that implements the RouteAware Mixin.
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
return CupertinoApp(
...
navigatorObservers: [routeObserver],
}
Pushing like so:
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => MyWidget()),
);
The Widget
class MyWidget extends StatefulWidget {
MyWidget({Key key}) : super(key: key);
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with RouteAware {
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context));
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPop() {
print('never called');
}
#override
void didPopNext() {
print('never called');
}
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
CupertinoNavigationBar(
middle: Text('42'),
),
],
),
);
}
}
The didPop() method is never called when popping a CupertinoPageRoute with the back button unless I push with rootNavigator = true which is not what I want.
How can I fix this?
EDIT: This behavior only happens when pushing routes from a CupertinoTabView, as it creates a new nested navigator.
Github Issue
If I understand correctly, the final solution shown in the Medium article does not extend every StatefulWidget with RouteAware. Instead, it extents its own implementation of a RouteObserver to get notified about every route change.
Using this approach, you should be able to insert your custom RouteObserver into both you main App as well as your CupertinoTabView to achieve what you need.
Related
I am trying to show a screen in flutter which should appear only for 10 seconds and then should disapper going back to previous screen. How this should be done in the correct manner in flutter. Is it possible to use timer.periodic in Flutter?
In the initstate of the screen that you wish to close add
Future.delayed(Duration(seconds: 10), (){
//Navigator.pushNamed("routeName");
Navigator.pop(context);
});
You can create new screen with Future.delayed inside initState
class NewPage extends StatefulWidget {
NewPage({Key? key}) : super(key: key);
#override
State<NewPage> createState() => _NewPageState();
}
class _NewPageState extends State<NewPage> {
#override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 10)).then((value) {
Navigator.of(context).pop();
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
);
}
}
I want to push two pages to the flutter navigator one after another, so that going back from 2nd page redirects me to the first page. The code for this action will look somewhat like below -
Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
The above code works fine. But my confusion is, will it work always as the pushNamed function is asynchronous as it returns a future value. So it could happen that the second page got pushed to navigator before the first page.
The ideal implementation seems to me to wait for the first call to pushNamed return its value and then call the second one. But strangely the following two solutions don't work. The first page did get pushed but it doesn't push the second page.
Solution 1(Not working):
Navigator.of(context).pushNamed(
FirstPage.PATH.then((_) =>
Navigator.of(context).pushNamed(SecondPage.PATH));
Solution 2(Not working):
await Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
Can anyone please clarify what I'm thinking wrong? Any help will be much appreciated. Thanks in Advance!
As an option you can pass a callback to pageA, add animation listener and call this callback when animation is finished.
this is full example:
import 'package:flutter/material.dart';
main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
routes: {
'pageA': (context) => PageA(),
'pageB': (context) => PageB(),
},
);
}
}
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: _onPressed,
child: Text('press me'),
),
),
);
}
void _onPressed() {
Navigator.of(context).pushNamed(PageA.routeName, arguments: _pushNextPage);
}
void _pushNextPage() {
Navigator.of(context).pushNamed(PageB.routeName);
}
}
class PageA extends StatefulWidget {
static const routeName = 'pageA';
#override
_PageAState createState() => _PageAState();
}
class _PageAState extends State<PageA> {
#override
void initState() {
super.initState();
WidgetsBinding.instance?.addPostFrameCallback((_) {
ModalRoute.of(context)?.animation?.addStatusListener(_statusListener);
});
}
void _statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
final route = ModalRoute.of(context);
route?.animation?.removeStatusListener(_statusListener);
final callback = route?.settings.arguments as VoidCallback;
callback.call();
}
}
#override
Widget build(BuildContext context) => Scaffold(body: Center(child: Text('PAGE A')));
}
class PageB extends StatelessWidget {
static const routeName = 'pageB';
#override
Widget build(BuildContext context) => Scaffold(body: Center(child: Text('PAGE B')));
}
Your solutions do not work, because the Future returned by pushNamed is only completed when the page is removed from the navigation stack again.
So in your examples, the second page is pushed, once the first page has been closed.
I don't think it can happen, that the second page will be pushed before the first page in this example:
Navigator.of(context).pushNamed(FirstPage.PATH);
Navigator.of(context).pushNamed(SecondPage.PATH);
This should be a valid solution for what you want to achieve.
Minimal reproducible code:
void main() => runApp(FooApp());
class FooApp extends StatefulWidget {
#override
_FooAppState createState() => _FooAppState();
}
class _FooAppState extends State<FooApp> {
bool _showPage2 = false;
void _onPressed(bool value) => setState(() => _showPage2 = value);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Navigator(
onPopPage: (route, result) => route.didPop(result),
pages: [
MaterialPage(child: Page1(onPressed: _onPressed)),
if (_showPage2) MaterialPage(child: Page2()),
],
),
);
}
}
class Page1 extends StatelessWidget {
final ValueChanged<bool> onPressed;
const Page1({Key key, this.onPressed}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Page1')),
body: ElevatedButton(
onPressed: () => onPressed(true),
child: Text('Page2'),
),
);
}
}
class Page2 extends StatefulWidget {
#override
_Page2State createState() => _Page2State();
}
class _Page2State extends State<Page2> {
void dispose() {
super.dispose();
print('dispose');
}
Widget build(BuildContext context) => Scaffold(appBar: AppBar(title: Text('Page2')));
}
didPop:
When this function returns true, the navigator removes this route from the history but does not yet call dispose. Instead, it is the route's responsibility to call NavigatorState.finalizeRoute, which will in turn call dispose on the route. This sequence lets the route perform an exit animation (or some other visual effect) after being popped but prior to being disposed.
But in my example, you can see without having to call NavigatorState.finalizeRoute in Page2, dispose method does get called contradiciting Docs.
It's done internally when using MaterialPage/_PageBasedMaterialPageRoute. You can poke around in the code starting in the Navigator class, which appears to lead up to this OverlayRoute class. If you do want to trace through yourself, it wasn't a walk in the park for me and you'll have to pay close attention to how each class is related.
This class has the finishedWhenPopped getter, which is true by default. And if you look at the didPop override implementation right below the getter definition, didPop will internally call finalizeRoute when finishedWhenPopped is true.
Implementation from OverlayRoute class
#protected
bool get finishedWhenPopped => true;
#override
bool didPop(T? result) {
final bool returnValue = super.didPop(result);
assert(returnValue);
if (finishedWhenPopped)
navigator!.finalizeRoute(this);
return returnValue;
}
This is true only for at least MaterialPage/_PageBasedMaterialPageRoute. Other implementations don't necessarily do this.
I've:
class _PageState extends State<Page> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
print('state = $state');
}
#override
Widget build(BuildContext context) => Scaffold();
}
AppLifeCycleState class has got 4 callbacks, 3 of them
- active
- paused
- resumed
Seems to work but detached never worked in any case.
I read the documentation but couldn't understand it in practical scenario, can anyone share a relevant code, when and where does it get called?
As the doc says
detached → const AppLifecycleState The application is still hosted on
a flutter engine but is detached from any host views.
When the application is in this state, the engine is running without a
view. It can either be in the progress of attaching a view when engine
was first initializes, or after the view being destroyed due to a
Navigator pop.
You can reproduce above issue on HomeScreen only when your home widgets go in the background(Press back button of android device)
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
didChangeAppLifecycleState(AppLifecycleState state) {
if (AppLifecycleState.paused == state) {}
print("Status :" + state.toString());
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Book'),
),
body: Center(
child: Text('Home Screen'),
),
);
}
}
You can produce above mention thing on other screens and have a call at detached, you can do by closing application programmatically on any click event of your widget
Android:
SystemChannels.platform.invokeMethod('SystemNavigator.pop');
iOS:
exit(0)
It's a bug. I've tests on both iOS and Android devices. You can produce detached state when you closed the app by pressing the back button or swiping on Android devices, it's not woking on iOS devices. Please follow this issue on this
I am facing an issue in which dispose method is not called after changing screen in flutter .First of all here is the source code.
class Game extends StatefulWidget {
Game({Key key, this.title}) : super(key: key);
final String title;
#override
_GameState createState() => new _GameState();
}
class _GameState extends State<Game> with SingleTickerProviderStateMixin {
final CrosswordController myController = CrosswordController();
var chewieController = null;
var videoPlayerController = null;
Widget makeVideoStreaming(){
videoPlayerController = VideoPlayerController.network("https://somelink.com");
chewieController = ChewieController(//paramtere here
);
}
#override
void initState() {
super.initState();
this.makeVideoStreaming();
_controller = AnimationController(vsync: this, duration: Duration(minutes: gameTime));
}
#override
void dispose(){
print('DISPOSE CALLED- GAME---');
videoPlayerController.dispose();
chewieController.dispose();
_controller.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
child: Scaffold(
key: _scaffoldKey,
drawer: NavigationDrawer(),
resizeToAvoidBottomPadding: false,
body://body here
),
);
}
}
In NavigationDrawer() i changes to some different route something like this.
onTap: () {
Navigator.pop(context); Navigator.pushNamed(context, '/edit_profile');
},
Above is just a part of code which is called after clicking on one of the item from drawer list item.
In GameState dispose method is not called why ?
Dispose method called when you remove screen from stack mean's that when you use navigator.pop() Or pushReplacement;
it's a known bug: https://github.com/flutter/flutter/issues/40940
even if you copy examples from official docs, it will not get called: https://flutter.dev/docs/cookbook/networking/web-sockets#complete-example
I've started a thread of flutter-dev to find out if something else should be used instead: https://groups.google.com/g/flutter-dev/c/-0QZFGO0p-g/m/bictyZWCCAAJ