Make a transition when reordering pages in Navigator.pages - flutter

I have a list of pages in Navigator.pages. When I push a page with a duplicate key, I want to bring the old page on top. The problem is, the page shows without animation, it just appears on top instantly. Meanwhile the default animation works alright when pushing a new page for the first time. How do I animate the page if it is brought on top?
Here is the code:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
final _notifier = ChangeNotifier();
final _pages = <Page>[Page1()];
class Page1 extends MaterialPage {
Page1()
: super(
key: const ValueKey('Page1'),
child: Scaffold(
appBar: AppBar(title: const Text('Page1')),
body: const Center(
child: ElevatedButton(
onPressed: _showPage2,
child: Text('Show Page2'),
),
),
),
);
}
class Page2 extends MaterialPage {
Page2()
: super(
key: const ValueKey('Page2'),
child: Scaffold(
appBar: AppBar(title: const Text('Page2')),
body: const Center(
child: ElevatedButton(
onPressed: _showPage1,
child: Text('Show Page1'),
),
),
),
);
}
void _showPage1() {
for (final page in _pages) {
if (page.key == const ValueKey('Page1')) {
_pages.remove(page);
break;
}
}
_pages.add(Page1());
_notifier.notifyListeners();
}
void _showPage2() {
for (final page in _pages) {
if (page.key == const ValueKey('Page2')) {
_pages.remove(page);
break;
}
}
_pages.add(Page2());
_notifier.notifyListeners();
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomeScreen(),
);
}
}
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _notifier,
builder: (_, __) {
return Navigator(
pages: [..._pages],
transitionDelegate: const MyTransitionDelegate(),
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
return true;
},
);
},
);
}
}
What I tried:
Navigator.transitionDelegate seemed like the property to handle this. But it is only invoked when a new route is pushed and not when the order is changed.
To dispose the old route before bringing the page on top in hope that it will be re-created by Flutter and that could somehow trigger the animation. But I just get an exception saying the route cannot be used after disposal.
To let the navigator rebuild without the old page with _changeNotifier.notifyListeners(); await Future.delayed(Duration.zero); before re-adding the page. But the zero duration did not work.
The workarounds so far are:
To add a non-zero duration between removing and re-adding a page. But this is noticeable.
To change the key of the new page. But I need keys to track some things in the app.

Related

Show Snackbar on top route after closing a page

In my app, I have some areas where I can open a new page on top of the current, that allow to edit data. Once editing is done, I want to close the page (i.e. via Navigator.pop(context);), and also show a Snackbar after closing (i.e. via ScaffoldMessenger.of(context).showSnackBar('X has been saved')). I am using a ScaffoldMessenger for that.
However, if after closing the edit-page only the top-route remains, the Snackbar will not be shown. If I open any other page fast enough, it will be shown there for the remaining time though. So it was triggered, it is just not shown on the top-route. Also, if I open the edit-page not from the top-route, but from any other page that was already opened on top, the Snackbar will show normally after closing the edit-page.
If I open a Snackbar directly on the top-route, it also works fine. So instead of opening the Snackbar from the edit-page, I could technically return the message and then trigger the Snackbar. But I would prefer not to to pass data around and call functionality at several places, but just call the method at one place (where it belongs).
I can reproduce this behaviour on a newly created App, just need to replace the _MyHomePageState with the following code. What am I doing wrong here?
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey<ScaffoldMessengerState> _globalScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
#override
Widget build(BuildContext context) {
return ScaffoldMessenger(
key: _globalScaffoldMessengerKey,
child: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push<bool>(context, MaterialPageRoute(builder: (context) => const SubPage()));
},
child: const Text("Open Subpage"),
),
),
),
);
}
}
class SubPage extends StatelessWidget {
const SubPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Hello Snackbar')));
},
child: const Text("Close Subpage"),
),
),
);
}
}
Remove the scaffold Messenger widget from the first page
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({key});
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => const SubPage()));
},
child: const Text("Open Subpage"),
),
),
);
}
}
class SubPage extends StatelessWidget {
const SubPage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Hello Snackbar')));
},
child: const Text("Close Subpage"),
),
),
);
}
}
I checked this code and it shows snackbar in the page that exists after popping the subpage

How to show SnackBar in ValueNotifier state manager

I'm working on a PageView with States managed by ValueNotifier. I choose ValuNotifier because it is fast and native. The ValueListenableBuilder works great for a regular page with states like loading, success and error where the body is rebuilt with the content state.
In the code above, ValueListenableBuilder rebuild the body page when state changes, but some states should only push SnackBar and should keep the current value.
What is the best way to handler error (or warning) in SnackBar or Dialog, keeping the body page with the current state (with data, for example)?
All states (even error) carry all data to rebuild the body, than I show SnackBar and rebuild the body;
Show SnackBar by one callback, viewpage will register one controller callback and do all the process to get the context and show SnackBar;
In my point of view, the 2nd distort the ideia of state manager but avoid to rebuild body; but the 1st looks over by the fact I have to carry all information all the time and 'rebuild' everything. I think, the 1st could be a big problem if body has animated transitions.
Do you recomend a 3rd alternative?
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
// ======= Controller =======
class CounterState {}
class SuccessCounterState extends CounterState {
final int currentValue;
SuccessCounterState(this.currentValue);
}
class MaxCounterState extends CounterState {}
class MinCounterState extends CounterState {}
class Counter extends ValueNotifier<CounterState> {
Counter() : super(SuccessCounterState(0));
var _localValue = 0;
void increment() {
if (_localValue + 1 > 9) {
value = MaxCounterState();
} else {
_localValue++;
value = SuccessCounterState(_localValue);
}
}
void decrement() {
if (_localValue - 1 < 0) {
value = MinCounterState();
} else {
_localValue--;
value = SuccessCounterState(_localValue);
}
}
}
// ==== Page ====
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final counter = Counter();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
ValueListenableBuilder<CounterState>(
valueListenable: counter,
builder: (BuildContext context, state, _) {
if (state is SuccessCounterState) {
return Text(
'${state.currentValue}',
style: Theme.of(context).textTheme.headline4,
);
}
if (state is MaxCounterState) {
Future.delayed(const Duration(milliseconds: 1), () {
const snackBar =
SnackBar(content: Text('Reached the max value'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}
if (state is MinCounterState) {
Future.delayed(const Duration(milliseconds: 1), () {
const snackBar =
SnackBar(content: Text('Reached the min value'));
ScaffoldMessenger.of(context).showSnackBar(snackBar);
});
}
return const SizedBox();
},
),
],
),
),
floatingActionButton: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton(
onPressed: counter.decrement,
tooltip: 'Decrement',
child: const Icon(Icons.remove),
),
const SizedBox(width: 20.0),
FloatingActionButton(
onPressed: counter.increment,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
],
),
);
}
}

How to show navigation bar on page which doesn't belong to this bar?

edit: SOLUTION
i used package custom_navigator
In navigation bar I have 2 pages to redirect, but I want to navigate to third page and still want to see navigation bar (this one with 2 pages) there.
Is it possible to do? Do I have to make my own navigation bar for this page?
class Bar extends StatefulWidget {
#override
BarState createState() => BarState();
}
class BarState extends State<Bar> {
int tabIndex = 0;
List<Widget> pages = [
FirstPage(),
SecondPage(),
];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: SafeArea(
child: BottomNavigationBar(
iconSize: 25,
elevation: 4.0,
items: <BottomNavigationBarItem>[
barItem(Icons.message),
barItem(Icons.camera_enhance),
barItem(Icons.person),
],
currentIndex: tabIndex,
onTap: (int index) {
setState(() {
tabIndex = index;
});
},
),
)),
body: Container(
child: pages.elementAt(tabIndex),
),
);
}
}
this is what i try:
List<Widget> pages = [
Container(
color: Colors.green,
child: Center(
child: ElevatedButton(
onPressed: state
),
),
),
SecondPage(),
ThirdPage()
];
state() {
tabIndex = 2;
setState(() {
});
}
The simplest way, if you don't mind it animating would be to init an AppBar in your navigator and pass it to the pages and they would use it in there scaffold.
class MyFlow extends StatefulWidget {
const MyFlow({Key? key}) : super(key: key);
#override
_MyFlowState createState() => _MyFlowState();
}
class _MyFlowState extends State<MyFlow> {
#override
Widget build(BuildContext context) {
final appBar = AppBar();
return Navigator(
onPopPage: (route, result) => true,
pages: [
MaterialPage(child: PageOne(appBar: appBar)),
MaterialPage(child: PageTwo(appBar: appBar)),
MaterialPage(child: PageThree(appBar: appBar)),
],
);
}
}
class PageOne extends StatelessWidget {
const PageOne({Key? key, required this.appBar}) : super(key: key);
final AppBar appBar;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBar,
);
}
}

Flutter: How to close SnackBar when navigating away?

I have an issue. I have a "details" page which can display a SnackBar via ScaffoldMessenger.
This snackbar does not hide, its duration is set to long time because it's supposed to stay visible for a long time or until it's dismissed by user or by navigating away from the view.
The last part is the one I'm having issue with. In my dispose method I try to call ScaffoldMessenger.of(_scaffoldKey.currentContext!).hideCurrentSnackBar() but this does not work and throws error that it can't access the context.
I suspect it's because the context associated with the key is also the same context that is being removed from the widget tree since I'm navigating away from it (by using back button).
I do not want to handle removing of the snackbar in other views. I know I could probably call ScaffoldMessenger.of(context).clearAllSnackBars() every time I would navigate to them but I don't like it for architectural reasons:
The "detail" view owns the snackbar because it's responsible for creating it. It should be also responsible for disposing of it.
In future I might reorganize my views and then I have to remember to
clear the snackbar everywhere. The example I gave you is constrained
example but imagine there's accessible sidebar leading to many
different views. It would mean adding this code to all those views.
So I really want to somehow remove that snackbar when disposing of the DetailPage view. How can I achieve this?
Link to dartpad.dev
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: const HomePage(), routes: {
'/detail': (_) => const DetailPage(),
});
}
}
class HomePage extends StatelessWidget {
const HomePage({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: SizedBox.expand(
child: TextButton(
child: const Text('See detail'),
onPressed: () {
Navigator.pushNamed(context, '/detail');
},
),
),
);
}
}
class DetailPage extends StatefulWidget {
const DetailPage({
Key? key,
}) : super(key: key);
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
ScaffoldMessenger.of(_scaffoldKey.currentContext!).showSnackBar(SnackBar(
content: const Text('Entered detail page'),
duration: const Duration(days: 1),
action: SnackBarAction(
label: 'Close',
onPressed: () {
ScaffoldMessenger.of(_scaffoldKey.currentContext!)
.hideCurrentSnackBar();
}),
));
});
}
#override
void dispose() {
ScaffoldMessenger.of(_scaffoldKey.currentContext!).hideCurrentSnackBar();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(title: const Text('Detail')),
body: const SizedBox.expand(
child: Center(child: Text('Detail page')),
),
);
}
}
Use WillPopScope widget to remove the snackbar.
This widget allows async code to run before the view is popped of the navigation stack and the context is still present in the widget tree at that moment. You can get rid of the overriden dispose method this way.
You can see it working in this dartpad or just note the code below:
import 'package:flutter/material.dart';
void main() {
runApp(App());
}
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(home: const HomePage(), routes: {
'/detail': (_) => const DetailPage(),
});
}
}
class HomePage extends StatelessWidget {
const HomePage({
Key? key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: SizedBox.expand(
child: TextButton(
child: const Text('See detail'),
onPressed: () {
Navigator.pushNamed(context, '/detail');
},
),
),
);
}
}
class DetailPage extends StatefulWidget {
const DetailPage({
Key? key,
}) : super(key: key);
#override
_DetailPageState createState() => _DetailPageState();
}
class _DetailPageState extends State<DetailPage> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
ScaffoldMessenger.of(_scaffoldKey.currentContext!).showSnackBar(SnackBar(
content: const Text('Entered detail page'),
duration: const Duration(days: 1),
action: SnackBarAction(
label: 'Close',
onPressed: () {
ScaffoldMessenger.of(_scaffoldKey.currentContext!)
.hideCurrentSnackBar();
}),
));
});
}
#override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(title: const Text('Detail')),
body: const SizedBox.expand(
child: Center(child: Text('Detail page')),
),
),
onWillPop: () async {
ScaffoldMessenger.of(_scaffoldKey.currentContext!).hideCurrentSnackBar();
return Future.value(true);
});
}
}
I think the only downside is that hideCurrentSnackBar does not complete with Future so the animation does not finish. Maybe there'd be a way to do it with some sort of Completer.
try{ ScaffoldMessenger.of(context).show()/// show snackbar
}catch(e){
print(e);
}
putting scaffold messenger inside a try catch prevent disposed context usage error

Flutter - Best way to aggregate data from child widgets in an IndexedStack

I have an IndexedStack in a Scaffold that I use to manage my registration. The Registration widget itself is Stateful, but the widgets that compose it are Stateless. The parent widget looks like this:
class Registration extends StatefulWidget {
#override
_RegistrationState createState() => _RegistrationState();
}
class _RegistrationState extends State<Registration> {
int _index = 0;
void _nextPage() {
setState(() {
_index++;
});
}
void _prevPage() {
setState(() {
_index--;
});
}
#override
Widget build(BuildContext context) {
return new Scaffold(
backgroundColor: Colors.white,
appBar: new AppBar(
backgroundColor: Colors.white,
automaticallyImplyLeading: false,
leading: new IconButton(
icon: new Icon(Icons.arrow_back,
color: Theme.of(context).primaryColor),
onPressed: () {
if (_index == 0) {
Navigator.pop(context);
} else {
_prevPage();
}
}),
elevation: 0.0,
),
body: IndexedStack(
children: <Widget>[
RegistrationPhone(_nextPage),
RegistrationName(_nextPage),
RegistrationBirthday(_nextPage),],
index: _index,
),
);
}
}
What is the best way to take data from these child widgets?
Should I pass in a callback function and hold the data in the parent? Should I pass the information down the line from widget to widget until it's submitted? I don't know what the practices are for sharing data across multiple screens.
Use Provider
Add Dependency :
dependencies:
provider: ^4.3.3
here is the Example :
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
/// This is a reimplementation of the default Flutter application using provider + [ChangeNotifier].
void main() {
runApp(
/// Providers are above [MyApp] instead of inside it, so that tests
/// can use [MyApp] while mocking the providers
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => Counter()),
],
child: const MyApp(),
),
);
}
/// Mix-in [DiagnosticableTreeMixin] to have access to [debugFillProperties] for the devtool
// ignore: prefer_mixin
class Counter with ChangeNotifier, DiagnosticableTreeMixin {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
/// Makes `Counter` readable inside the devtools by listing all of its properties
#override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(IntProperty('count', count));
}
}
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Example'),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
Text('You have pushed the button this many times:'),
/// Extracted as a separate widget for performance optimization.
/// As a separate widget, it will rebuild independently from [MyHomePage].
///
/// This is totally optional (and rarely needed).
/// Similarly, we could also use [Consumer] or [Selector].
Count(),
],
),
),
floatingActionButton: FloatingActionButton(
key: const Key('increment_floatingActionButton'),
/// Calls `context.read` instead of `context.watch` so that it does not rebuild
/// when [Counter] changes.
onPressed: () => context.read<Counter>().increment(),
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
class Count extends StatelessWidget {
const Count({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Text(
/// Calls `context.watch` to make [Count] rebuild when [Counter] changes.
'${context.watch<Counter>().count}',
key: const Key('counterState'),
style: Theme.of(context).textTheme.headline4);
}
}