Add to Scaffold Appbar a click counter with multiple pages - flutter

Hello I'm creating an app with multiple pages using Navigators and routes.
I would like to add to the Scaffold Appbar a counter that increment every time a finger clicks on a screen button (also if they are more then one button present in the page).
Also if I change the pages, this counter must increase.
Can you help me to understand the issue?
I'm learing so probably the structure could be a "beginner" version.
Thanks.

As #Randal Schwartz commented, we could take advantage of the onGenerateRoute property of the MaterialApp class:
runApp(MaterialApp(
home: BuilderPage(LoginArguments(false)),
onGenerateRoute: generateRoute
));
By defining a custom generateRoute function:
int counter = 0; // global variable outside of the classes
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case 'home':
counter++;
return MaterialPageRoute(builder: (_) => HomeScreen());
case 'login':
counter++;
return MaterialPageRoute(builder: (_) => LoginScreen());
case 'register':
counter++;
return MaterialPageRoute(builder: (_) => RegisterScreen());
}
}
The counter could be then displayed by the Widgets by loading it in their state:
class HomeScreen extends StatefulWidget {
const HomeScreen({ Key key }) : super(key: key);
#override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
int _counter;
#override
void initState() {
super.initState();
setState(() => _counter = counter);
}
void _increaseCounter() {
counter++;
setState(() => _counter++);
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(child: Text(_counter.toString())),
body: SafeArea(
child: Center(child:
GestureDetector(
onTap: () { _increaseCounter(); },
child: Text('test'),
),
),
),
),
);
}
}
NB: this solution is not optimal. Please make sure to consider more advanced state management architectures, like BLoC, to better manage the data syncronization between different Widgets in the app.

You can probably hook some logging into the PageRouteBuilder, because every route goes through there from a given page (of course, it'd have to be on every page too).

Related

Detect enter the page event Flutter

Do we have any event trigger when user enter the page.
I found Navigator.push().then().
But seen it's very unconvenient.
I want to have somethings like initState, but trigger every time user enter the page.
In IONIC(hybrid frame work) its name is ionViewWillEnter
Thanks for your help!
This is not really more convenient than Navigator.push().then() but you could use a RouteObserver to detect the page changes.
Code
For this example I am going to define 2 global variables:
final routeObserver = RouteObserver<ModalRoute<void>>();
int count = 0; // Number of times you are entering the page
Then add routeObserver to your MaterialApp.navigatorObservers:
MaterialApp(
home: InitialPage(),
navigatorObservers: [routeObserver],
)
Finally, you will need to manage the subscription of your routeObserver to your page. For this you will have to use a StatefulWidget as your "enter on page" behavior will be defined thanks to the page's State:
class InitialPage extends StatefulWidget {
#override
State<InitialPage> createState() => _InitialPageState();
}
class _InitialPageState extends State<InitialPage> with RouteAware {
#override
void initState() {
super.initState();
count++;
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
#override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
#override
void didPopNext() {
super.didPopNext();
// view will appear
setState(() => count++);
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('You entered on this page $count times'),
ElevatedButton(
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => OtherPage(),
),
),
child: const Text('Press me'),
),
],
),
),
);
}
}
Basically what I am doing is incrementing the counter when instanciating the page thanks to initState (called when the page is added to the widget tree) and by registering the routeObserver to your view I will be able to increment my counter when my page is already in the widget tree with didPopNext (and using a setState to update my UI).
You can try the full example on DartPad

Flutter 2.0 Navigator w/ Provider Gives Null Value Errors when Popping

I am currently having some data race bugs when using the Flutter 2.0 Navigator API. My store is implemented with MobX and passed down via Provider. After that, I pull an Observer over the global store and then re-update the Navigator (for the routes) every time my global store updates. However, everything works fine until I hit the top back arrow on the WinnerPage. It shows the following error on the screen and flashes back to being fine instantly later:
Unexpected null value.
The relevant error-causing widget was WinnerView
Therefore, to try and debug further, I learned that the value of winner is null for an instant (from onPopPage in Navigator) when pressing the back button, which pops the state. Does anyone know if there is a fix to this problem? Here is all of my code:
Store
class GlobalStore extends Store {
#observable
String? winner;
#action
void setWinner(String? newWinner) => winner = newWinner;
}
App Setup
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: "MyApp",
home: Provider<GlobalStore>(
create: (_) => GlobalStore(),
child: Observer(builder: (context) {
final store = Provider.of<GlobalStore>(context);
return Navigator(
pages: [
FooPage(),
if (store.winner != null)
WinnerPage()
],
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
store.setWinner(null);
return true;
}
);
}),
)
);
}
}
Foo Page (default)
class FooPage extends Page {
const FooPage() : super(key: const ValueKey('Foo Page'));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this, builder: (BuildContext context) => FooView());
}
}
class FooView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final store = Provider.of<GlobalStore>(context);
return Scaffold(
body: TextButton(
child: const Text('Hello, world!'),
onPressed: () {
store.setWinner("You!");
}
)
);
}
}
Winner Page
class WinnerPage extends Page {
const WinnerPage() : super(key: const ValueKey('Winner Page'));
Route createRoute(BuildContext context) {
return MaterialPageRoute(
settings: this, builder: (BuildContext context) => WinnerView());
}
}
class WinnerView extends StatelessWidget {
#override
Widget build(BuildContext context) {
final store = Provider.of<GlobalStore>(context);
final winner = store.winner!; // error here!
return Scaffold(
appBar: AppBar(),
body: Column(children: [Text('${winner} is the winner!')]));
}
}
I actually solved this myself, although I cannot find a complete explanation for why the problem was happening so if anyone else has a better answer please write it. Anyways, It looks like for some reason my way of extending Page was not working correctly. Therefore, I changed each of my Navigator.pages to look like this:
[
// note: I am using the stateless widget views not pages
MaterialPage(child: const FooView(), key: const ValueKey("FooPage")),
if (store.winner != null)
MaterialPage(child: const WinnerView(), key: const ValueKey("WinnerPage"))
]
I also have a hunch that the problem was that I was only extending Page, not MaterialPage, which may not have the same behavior. However, I still don't know why it wasn't working the other way after ~1 hr of searching.

Flutter pushing two page to navigator sequentially

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.

Flutter CircularprogressIndicator with Navigation

How to implement flutter code so that as soon as my application is launched, it will show circularprogressindicator and then load another class through Navigation.push
As I know navigation.push requires a user action like ontap or onpressed
Please assist me with this
The requirement you need is of Splash Screen, which stays for a while, and then another page comes up. There are certain things you can do, that is
Use Future.delayed constructor, which can delay a process by the Duration time you provide, and then implement your OP, in this case, you Navigator.push()
Future.delayed(Duration(seconds: your_input_seconds), (){
// here you method will be implemented after the seconds you have provided
Navigator.push();
});
The above should be called in the initState(), so that when your page comes up, the above process happens and you are good do go
You can use your CircularProgressIndicator normally in the FirsScreen
Assumptions:
Our page will be called FirstPage and SecondPage respectively.
We will be going from FirstPage -> SecondPage directly after N seconds
Also, if you are working on a page like this, you don't want to go back to that page, so rather than using Navigator.push(), use this pushAndRemoveUntil
Let us jump to the code now
FirstPage.dart
// FIRST PAGE
class FirstPage extends StatefulWidget {
FirstPage({Key key, this.title}) : super(key: key);
final String title;
#override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
//here is the magic begins
#override
void initState(){
super.initState();
//setting the seconds to 2, you can set as per your
// convenience
Future.delayed(Duration(seconds: 2), (){
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
builder: (context) => SecondPage()
), (_) => false);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Container(
height: double.infinity,
width: double.infinity,
child: Center(
child: CircularProgressIndicator()
)
)
);
}
}
SecondPage.dart
// SECOND PAGE
class SecondPage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Page"),
),
body: Container(
height: double.infinity,
width: double.infinity,
child: Center(
child: Text('Welcome to Second Page')
)
)
);
}
}
Result
Look at how the page works, with out having any buttons, stays for 2 seconds and then go to second page. But also, no back button, since going back is not the right choice. You must remove all the items from the stack if you are making a page like this
EDITS AS PER THE ERROR
Since I can see that you're currently getting an error because, the Widget is not ready, to even call the Future.delayed(). To do that what you need to do is, make changes in your FirstPage.dart, initState() method. Rest can left as is
#override()
void initState(){
super.initState();
// Ensures that your widget is first built and then
// perform operation
WidgetsBinding.instance.addPostFrameCallback((_){
//setting the seconds to 2, you can set as per your
// convenience
Future.delayed(Duration(seconds: 2), (){
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
builder: (context) => SecondPage()
), (_) => false);
});
});
}
OR
If WidgetsBinding.instance.addPostFrameCallback((_){}, this doesn't comes handy, use this in place of the mentioned function
// This needs to be imported for this particular only
// i.e., ScheduleBider not WidgetBinding
import 'package:flutter/scheduler.dart';
#override
void initState(){
super.initState();
SchedulerBinding.instance.addPostFrameCallback((_){
//setting the seconds to 2, you can set as per your
// convenience
Future.delayed(Duration(seconds: 2), (){
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(
builder: (context) => SecondPage()
), (_) => false);
});
});
}

How to remove the first screen from route in Flutter?

I am creating a loading screen for an app. This loading screen is the first screen to be shown to the user. After 3 seconds the page will navigate to the HomePage. everything is working fine. But when the user taps back button the loading screen will be shown again.
FIRST PAGE CODE
import 'dart:async';
import 'package:flutter/material.dart';
import 'home_page.dart';
void main() {
runApp(MaterialApp(
home: MyApp(),
));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
Future.delayed(
Duration(
seconds: 3,
), () {
// Navigator.of(context).pop(); // THIS IS NOT WORKING
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FlutterLogo(
size: 400,
),
),
);
}
}
HOMEPAGE CODE
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: Text('HomePage'),
),
),
);
}
}
I tried to add Navigator.of(context).pop(); before calling the HomePage but that is not working. This will show a blank black screen.
Any ideas??
You need to use pushReplacement rather than just push method. You can read about it from here: https://docs.flutter.io/flutter/widgets/Navigator/pushReplacement.html
And to solve your problem just do as explain below.
Simply replace your this code:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
with this:
Navigator. pushReplacement(
context,
MaterialPageRoute(
builder: (context) => HomePage(),
),
);
Yes, I found the same problem as you. The problem with replace is that it only works once, but I don't know why it doesn't work as it should. For this after a few attempts, I read the official guide and this method exists: pushAndRemoveUntil (). In fact, push on another widget and at the same time remove all the widgets behind, including the current one. You must only create a one Class to management your root atrough the string. This is the example:
class RouteGenerator {
static const main_home= "/main";
static Route<dynamic> generatorRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case main_home:
return MaterialPageRoute(builder: (_) => MainHome());
break;
}
}
}
This class must be add to the Main in:
MaterialApp( onGenerateRoute: ->RouteGenerator.generatorRoute)
Now to use this method, just write:
Navigator.of(context).pushNamedAndRemoveUntil(
RouteGenerator.main_home,
(Route<dynamic> route) => false
);