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.
Related
I'm trying to create a custom menu bar in my app. Right now, the biggest issue I'm having is passing a state for when it's expanded to it's children after a setState occurs.
I thought about inheritance, but from what I've tried all inheritance needs to be in-line. I can't create a widget where the children [] are fed into the constructor on an ad-hoc basis.
My current approach is to use a GlobalKey to update the State of the children widgets being inserted into the StateFul while updating them directly.
The children for my MenuBar are declared as:
List<MenuBarItem> menuItems;
MenuBarItem is an abstract interface class that I intend to use to limit the widgets that can be fed in as menuItems to my MenuBar.
abstract class iMenuItem extends Widget{}
class MenuBarItem extends StatefulWidget implements iMenuItem{
At some iterations of this script, I had a bool isExpanded as part of the iMenuItem, but determined it not necessary.
Here is my code at its current iteration:
My Main:
void main() {
// runApp(MainApp());
//runApp(InherApp());
runApp(MenuBarApp());
}
class MenuBarApp extends StatelessWidget{
#override
Widget build(BuildContext context){
return MaterialApp(
home: Scaffold(
body: MenuBar(
menuItems: [
// This one does NOT work and is where I'm trying to get the
// value to update after a setState
MenuBarItem(
myText: 'Outsider',
),
],
),
),
);
}
}
My Code:
import 'package:flutter/material.dart';
/// Primary widget to be used in the main()
class MenuBar extends StatefulWidget{
List<MenuBarItem> menuItems;
MenuBar({
required this.menuItems,
});
#override
State<MenuBar> createState() => MenuBarState();
}
class MenuBarState extends State<MenuBar>{
bool isExpanded = false;
late GlobalKey<MenuBarContainerState> menuBarContainerStateKey;
#override
void initState() {
super.initState();
menuBarContainerStateKey = GlobalKey();
}
#override
Widget build(BuildContext context){
return MenuBarContainer(
menuItems: widget.menuItems,
);
}
}
class MenuBarContainer extends StatefulWidget{
List<MenuBarItem> menuItems;
late Key key;
MenuBarContainer({
required this.menuItems,
key,
}):super(key: key);
#override
MenuBarContainerState createState() => MenuBarContainerState();
}
class MenuBarContainerState extends State<MenuBarContainer>{
bool isExpanded = false;
#override
void initState() {
super.initState();
isExpanded = false;
}
#override
Widget build(BuildContext context){
List<Widget> myChildren = [
ElevatedButton(
onPressed: (){
setState((){
this.isExpanded = !this.isExpanded;
});
},
child: Text('Push Me'),
),
// This one works. No surprise since it's in-line
MenuBarItem(isExpanded: this.isExpanded, myText: 'Built In'),
];
myChildren.addAll(widget.menuItems);
return Container(
child: Column(
children: myChildren,
),
);
}
}
/// The item that will appear as a child of MenuBar
/// Uses the iMenuItem to limit the children to those sharing
/// the iMenuItem abstract/interface
class MenuBarItem extends StatefulWidget implements iMenuItem{
bool isExpanded;
String myText;
MenuBarItem({
key,
this.isExpanded = false,
required this.myText,
}):super(key: key);
#override
State<MenuBarItem> createState() => MenuBarItemState();
}
class MenuBarItemState extends State<MenuBarItem>{
#override
Widget build(BuildContext context){
GlobalKey<MenuBarState> _menuBarState;
return Row(
children: <Widget> [
Text('Current Status:\t${widget.isExpanded}'),
Text('MenuBarState GlobalKey:\t${GlobalKey<MenuBarState>().currentState?.isExpanded ?? false}'),
Text(widget.myText),
],
);
}
}
/// To give a shared class to any children that might be used by MenuBar
abstract class iMenuItem extends Widget{
}
I've spent 3 days on this, so any help would be appreciated.
Thanks!!
I suggest using ChangeNotifier, ChangeNotifierProvider, Consumer and context.read to manage state. You have to add this package and this import: import 'package:provider/provider.dart';. The steps:
Set up a ChangeNotifier holding isExpanded value, with a setter that notifies listeners:
class MyNotifier with ChangeNotifier {
bool _isExpanded = false;
bool get isExpanded => _isExpanded;
set isExpanded(bool isExpanded) {
_isExpanded = isExpanded;
notifyListeners();
}
}
Insert the above as a ChangeNotifierProvider in your widget tree at MenuBar:
class MenuBarState extends State<MenuBar> {
#override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => MyNotifier(),
child: MenuBarContainer(
menuItems: widget.menuItems,
));
}
}
After this you can easily read and write the isExpanded value from anywhere in your widget tree under the ChangeNotifierProvider, for example:
ElevatedButton(
onPressed: () {
setState(() {
final myNotifier = context.read<MyNotifier>();
myNotifier.isExpanded = !myNotifier.isExpanded;
});
},
child: Text('Push Me'),
),
And if you want to use this state to automatically build something when isExpanded is changed, use Consumer, which will be notified automatically upon every change, for example:
class MenuBarItemState extends State<MenuBarItem> {
#override
Widget build(BuildContext context) {
return Consumer<MyNotifier>(builder: (context, myNotifier, child) {
return Row(
children: <Widget>[
Text('Current Status:\t${myNotifier.isExpanded}'),
Text(widget.myText),
],
);
});
}
}
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.
From the docs of addPostFrameCallback, I see that
Post-frame callbacks cannot be unregistered. They are called exactly
once.
and so I wonder if addPostFrameCallback can be called after the widget is disposed since it cannot be unregistered?
I think the _postFrameCallback's callback will be called after the widget is disposed.
View the Flutter source code:
firstly, Post-frame callbacks cannot be unregistered. They are called exactly once. event registered after widget disposed, but still can't unregister.
secondly, when a new frame comes down, Flutter framework just check callback whether null, and then call the callback directly.
void handleBeginFrame(Duration? rawTimeStamp) {
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
}
void _invokeFrameCallback(FrameCallback callback, Duration timeStamp, [ StackTrace? callbackStack ]) {
assert(callback != null);
callback(timeStamp);
}
So, the callback will be called.
But, you can't update the UI (setState) in the callback because widgets have already been disposed.
example:
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: Test(),
);
}
}
class Test extends StatefulWidget {
#override
_TestState createState() => _TestState();
}
class _TestState extends State<Test> {
bool isEnabled = false;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child:Listener(
onPointerDown: (event) {
setState(() {
isEnabled = true;
});
},
onPointerUp: (event) {
setState(() {
isEnabled = false;
});
},
child: Container(
height: 200,
width: 300,
color: Colors.red,
child: isEnabled ? Test1() : Text("null"),
),
),
),
);
}
}
class Test1 extends StatefulWidget {
#override
_Test1State createState() => _Test1State();
}
class _Test1State extends State<Test1> {
#override
Widget build(BuildContext context) {
return Text("hahah");
}
_onFrameStart(Duration duration) {
print("in _onFrameStart");
}
#override
void dispose() {
SchedulerBinding.instance.addPostFrameCallback(_onFrameStart);
super.dispose();
}
}
We want to show an AlertDialog after some asynchronous processing such as network processes.
When calling 'showAlertDialog ()' from an external class, I want to call it without context. Is there a good way?
class SplashPage extends StatelessWidget implements SplashView {
BuildContext _context;
#override
Widget build(BuildContext context) {
this._context = context;
...
}
I've considered the above method, but I'm worried about side issues.
Help
My current code
class SplashPage extends StatelessWidget implements SplashView {
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyStoreColors.eats_white1_ffffff,
body: Center(
child: new SvgPicture.asset('assets/ic_splash.svg'),
),
);
}
#override
void showAlertDialog() {
showDialog<void>(
context: /*How to get context?*/,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Not in stock'),
content: const Text('This item is no longer available'),
actions: <Widget>[
FlatButton(
child: Text('Ok'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
}
#override
void moveToHomeContainer() {
}
#override
void moveToLoginContainer() {
}
}
To show an AlertDialog you need the context, but in StatelessWidget you do not have access to it directly as in StatefulWidget.
Few options are [1]:
passing it as GlobalKey [2]
passing build context as parameter to any other function inside StatelessWidget
use a service to inject the dialog without context [3]
Cheers.
You should trigger rebuild when the async event complete, either convert your widget to StatefulWidget and call setState() or use a state management solution like Bloc.
For example using StatefulWidget your code will look like this:
class SplashPage extends StatefulWidget {
#override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage> implements SplashView {
bool _asynOpDone = false;
/// Call this when the async operation is done.
void _onAsynOpDone() => setState(() => _asyncOpDone = true);
#override
Widget build(BuildContext context) {
if (_asyncOpDone) showAlertDialog(context);
return Scaffold(
...,
///
);
}
#override
void showAlertDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: ...,
);
}
}
You can apply Builder pattern concept to simplify this.
There is a little example here.
button_builder.dart
I am trying to refresh the parent widget from sub children widget. Actually, there are a number of widgets in between like A uses B and B uses C. I would like to refresh A widget on an event in C widget.I researched a lot but couldn't find an exact answer. A code snipped will be really helpful. Thanks in advance
There are a few solutions:
A pass a callback that does a setState to B, which then pass it to C:
class A extends StatefulWidget {
#override
_AState createState() => _AState();
}
class _AState extends State<A> {
#override
Widget build(BuildContext context) {
return B(
onSomething: () => setState(() {}),
);
}
}
class B extends StatelessWidget {
final VoidCallback onSomething;
const B({Key key, this.onSomething}) : super(key: key);
#override
Widget build(BuildContext context) {
return C(onSomething: onSomething);
}
}
class C extends StatelessWidget {
final VoidCallback onSomething;
const C({Key key, this.onSomething}) : super(key: key);
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: onSomething,
);
}
}
use NotificationListener in A, and dispatch a Notification from C:
class MyNotification extends Notification {}
class A extends StatefulWidget {
#override
_AState createState() => _AState();
}
class _AState extends State<A> {
#override
Widget build(BuildContext context) {
return NotificationListener<MyNotification>(
onNotification: (_) {
setState(() {});
},
child: B(),
);
}
}
class C extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
MyNotification().dispatch(context);
},
);
}
}
Why do you want to refresh? You updated any data and wanted the new ones to be displayed?
You could try to use Provider widget. With it you can modify any data and notify everyone interested in that data that it changed.
In your setup you could put the provider on the A widget, on the C widget you could get the value, updated and notify everyone. When doing that, A widget will automatically rebuild with the updated information.
The code would be something like this:
class AppState with ChangeNotifier {
AppState();
YourData _data;
void setData(YourData data) {
_data = data;
notifyListeners();
}
}
ChangeNotifierProvider.value(
value: AppState(),
child: WidgetA()
)
WidgetC() {
Provider.of<AppState>(context).setData(yourChangedDataHere);
}