Flutter: How to close SnackBar when navigating away? - flutter

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

Related

Make a transition when reordering pages in Navigator.pages

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.

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

Why state change error occurs on flutter_riverpod during initialization

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final dataProvider = StateNotifierProvider<DataNotifier, List<int>>((ref) {
return DataNotifier();
});
class DataNotifier extends StateNotifier<List<int>> {
DataNotifier() : super([]);
Future<void> getData() async {
state = [];
await Future.delayed(const Duration(seconds: 2));
state = [1, 2];
}
}
void main() => runApp(ProviderScope(child: App()));
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Home(),
);
}
}
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
color: Colors.white,
child: Center(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => SecondPage()),
);
},
child: const Text('Next page'),
),
),
),
);
}
}
class SecondPage extends ConsumerStatefulWidget {
const SecondPage({Key? key}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
class _SecondPageState extends ConsumerState<SecondPage> {
#override
void initState() {
super.initState();
ref.read(dataProvider.notifier).getData();
}
#override
Widget build(BuildContext context) {
final numbers = ref.watch(dataProvider);
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (_, index) {
return Text('data: $index');
},
itemCount: numbers.length,
),
);
}
}
I am new to riverpod and I noticed this error while changing state.
In the above code when I tap the "next page" button at the fresh start for the first time it works as expected but when I go back and again tap the "next page" button, an error shown below is thrown:
StateNotifierListenerError (At least listener of the StateNotifier Instance of 'DataNotifier' threw an exception
when the notifier tried to update its state.
Does anyone know why this occurs and how can I prevent it.
You can solve the issue using autoDispose
final dataProvider = StateNotifierProvider.autoDispose<DataNotifier, List<int>>(
(ref) => DataNotifier(),
);
For Future I prefer using FutureProvider.
More about riverpod

how to stop navigator to reload view?

I'm new to flutter and have a question about navigator.
I have 2 views one called Home and List. I created a drawer that is persistent in these two views. In each view I'm creating a reference to Firebase using FutureBuilder. The problem I'm running into is that every time I go to either Home or List initState is being called again. I believe the problem comes from selecting the page from the drawer. My question How can I still move to different pages without having to called InitState everytime I change screens.
title: Text('Go to page 1'),
onTap: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) => Listdb()));
This is where I think the screen rebuilds itself. Is there a way to avoid rebuilding?
Thank you for your help!
You can use the AutomaticKeepAliveClientMixin to prevent reloading everytime you change page, combining with PageView for better navigation. I'll included an example here:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
final PageController _pageController = PageController();
Widget build(BuildContext context) {
return Scaffold(
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: <Widget>[
DrawerHeader(
child: Text('Drawer Header'),
decoration: BoxDecoration(
color: Colors.blue,
),
),
ListTile(
title: Text('Item 1'),
onTap: () {
_pageController.jumpToPage(0);
Navigator.pop(context);
},
),
ListTile(
title: Text('Item 2'),
onTap: () {
_pageController.jumpToPage(1);
Navigator.pop(context);
},
),
],
),
),
body: PageView(
controller: _pageController,
children: <Widget>[
PageOne(),
PageTwo(),
],
),
);
}
}
class PageOne extends StatefulWidget {
#override
_PageOneState createState() => _PageOneState();
}
class _PageOneState extends State<PageOne> with AutomaticKeepAliveClientMixin {
#override
void initState() {
print("From PageOne - This will only print once");
super.initState();
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Colors.red,
);
}
}
class PageTwo extends StatefulWidget {
#override
_PageTwoState createState() => _PageTwoState();
}
class _PageTwoState extends State<PageTwo> with AutomaticKeepAliveClientMixin {
#override
void initState() {
print("From PageTwo - This will only print once");
super.initState();
}
#override
bool get wantKeepAlive => true;
#override
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
backgroundColor: Colors.blue,
);
}
}

Flutter - Update parant widget class UI on child button click

I have such kind of scenario
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Hello"),
),
body: Container(
child: ChildWidget(
listControl: this.sentToScreenBuildJson,
notifyParent: refresh,
),
),
);
}
this is my parent build method where I have added ChildWidget a another statfulscreen and passing is a json and a refresh funtion
as per json child will able to draw UI
and on button click I am able to get callback to refresh method.
refresh() {
print("I get refreshed from child");
setState(() {
print("I get refreshed from child in setState");
this.sentToScreenBuildJson = this.newJson;
});
}
on button click both print get execute but UI is not updating as per newJson.
Like I am expecting that as setState run parent has to call build with passing updated json.
which is not working.
thanks for any help.
When you want to pass data from Child to Parent you should use NotificationListener at parent and dispatch Notification from child.
Instance of Notification class will be having data that you can consume in Parent using NotificationListener.
Mostly all the Flutter Widgets are using this technique, for example tab controller receive OverscrollNotification when user reaches to the last tab and still try to swipe.
Following is the demo that you can use to understand how you can use NotificationListener in your code.
import 'package:flutter/material.dart';
void main() => runApp(ParentWidget());
class ParentWidget extends StatefulWidget {
ParentWidget({Key key}) : super(key: key);
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
String _text = 'You have not pressed the button yet';
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: NotificationListener<IntegerNotification>(
onNotification: (IntegerNotification notification) {
setState(() {
print(notification);
_text = 'You have pressed button ${notification.value} times';
});
return true;
},
child: Column(
children: <Widget>[
Text(_text),
ChildWidget(),
],
)
),
),
);
}
}
class ChildWidget extends StatefulWidget {
const ChildWidget({Key key}) : super(key: key);
#override
_ChildWidgetState createState() => _ChildWidgetState();
}
class _ChildWidgetState extends State<ChildWidget> {
int _counter = 0;
#override
Widget build(BuildContext context) {
return RaisedButton(onPressed: (){
IntegerNotification(++_counter).dispatch(context);
},child: Text('Increment counter'),);
}
}
#immutable
class IntegerNotification extends Notification{
final int value;
const IntegerNotification(this.value);
String toString(){
return value.toString();
}
}
Update parant widget class UI on child button click
This is a common use case in flutter and flutter has built in InheritedWidget class for these kind of purpose. You may either directly use it for your purpose or use some ready made package solution which uses InheritedWidget behind the scenes like Provider.
An alternative to #Darish's answer, you can declare a static variable in your class 1, access that static variable in class 2 and then update the state of the variable in the class 2.
For example:
import 'package:flutter/material.dart';
class Demo extends StatefulWidget {
static UserObject userObject;
#override
_Demo createState() => _Demo();
}
class _Demo extends State<Demo> {
#override
void initState() {
Demo.userObject = new UserObject(name: "EXAMPLE NAME");
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color(0xfff3f3f3),
appBar: AppBar(title: Text("DEMO")),
body: InkWell(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => HeroClass()));
},
child: Center(
child: Hero(
tag: "tag-demo-id",
child: Container(
color: Colors.black,
padding: EdgeInsets.all(20),
child: Text("${Demo.userObject.name} -> CLICK HERE",
style: TextStyle(color: Colors.white)))))));
}
}
class HeroClass extends StatefulWidget {
#override
_HeroClassState createState() => _HeroClassState();
}
class _HeroClassState extends State<HeroClass> {
final myController = TextEditingController();
#override
void initState() {
myController.text = Demo.userObject.name;
super.initState();
}
#override
void dispose() {
// Clean up the controller when the widget is removed from the widget tree.
// This also removes the _printLatestValue listener.
myController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("DEMO HERO")),
body: Hero(
tag: "tag-demo-id",
child: Container(
child: TextField(
controller: myController,
),
)),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
Demo.userObject.name = myController.text;
});
},
child: Icon(Icons.save),
));
}
}
// object class
class UserObject {
String name;
UserObject({this.name});
UserObject.fromJson(Map<String, dynamic> json) {
name = json['name'];
}
}