I have list of Dismissible widgets that I'd like to swipe left to delete. These have a an argument confirmDismis which takes a Future<bool>. I'd like to show a SnackBar once you've swiped an item allowing you to undo the delete for a few seconds. For this I have this method:
Future<bool> _confirmDismiss() {
var remove = true;
var duration = Duration(seconds: 5);
final snackbar = SnackBar(
duration: duration,
content: Text('This item was deleted'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
remove = false; //how to return false immediately?
},
),
);
_scaffoldKey.currentState.showSnackBar(snackbar);
return Future.delayed(duration,(){
return remove;
});
}
The snackbar is shown for 5 seconds, after that it disappears and the callback is triggered.
Now I'd like to return the item as soon as you hit the undo button. Right now you have to wait for the 5 seconds to pass for it to return.
I have tried timeout and Time() and many other things but I can't seem to make it work... Any ideas?
#Janneman, I don't think you need to use Future.delayed, that could be the reason why you now have to wait 5 secs. Just use the default SnackBar functions. Here is a quick example that shows SnackBar for 5 secs and quickly changes the text on Undo,
class MyApp extends StatefulWidget {
#override
MyAppState createState() => MyAppState();
}
class MyAppState extends State<MyApp> {
String text = 'Yay! A Snackbar';
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('SnackBar'),
),
body: Builder(builder: (BuildContext context) {
return Padding(
padding: EdgeInsets.all(20.0),
child: Center(
child: Column(children: <Widget>[
Text(text),
RaisedButton(
onPressed: () {
text = 'Snackbar changed';
setState(() {
});
final snackBar = SnackBar(
duration: Duration(seconds: 5),
content: Text(text),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
text = 'Yay! A Snackbar';
setState(() {});
},
),
);
// Find the Scaffold in the widget tree and use
// it to show a SnackBar.
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text('Show SnackBar'),
),
])));
})));
}
}
Hope this helps. Good luck!
Related
I have added following code to my main screen in order to exit app after 2 times back button taps
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
final timeGap = DateTime.now().difference(preBackpress);
final cantExit = timeGap >= const Duration(seconds: 2);
preBackpress = DateTime.now();
if(cantExit){
const snack = SnackBar(
content: Text('Press Back button again to Exit'),
duration: Duration(seconds: 2),
);
ScaffoldMessenger.of(context).showSnackBar(snack);
return false;
}else{
return true;
}
},
child: Scaffold(....)
);
}
But what I get instead is that when I hit back button it goes all the steps back to every single screen user has been visited.
The reason I added WillPopScope above was to avoid going back to visited screen and just close the app but it seems still doesn't help.
My question is: How do I exit app (when in main screen back button is tapped) without going back to visited screens?
Maybe this is not the same as your code. but I have code to close apps as you want
class ExitController extends StatefulWidget {
#override
_ExitControllerState createState() => _ExitControllerState();
}
class _ExitControllerState extends State<ExitController> {
static const snackBarDuration = Duration(milliseconds: 2000);
// set how long the snackbar appears
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
DateTime? backButtonPressTime;
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
// margin: EdgeInsets.fromLTRB(75, 0, 75, 250),
// add margin so that the snackbar is not at the bottom
duration: snackBarDuration,
backgroundColor: Colors.black87,
content: Text('Press Again to Exit',
textAlign: TextAlign.center,
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)),
);
Future<bool> onWillPop() async {
DateTime currentTime = DateTime.now();
bool backButtonCondition =
backButtonPressTime == null ||
currentTime.difference(backButtonPressTime!) > snackBarDuration;
if (backButtonCondition) {
backButtonPressTime = currentTime;
ScaffoldMessenger.of(context).showSnackBar(snackBar);
return false;
}
return true;
}
#override
Widget build(BuildContext context) {
return Scaffold(
key: scaffoldKey,
body: WillPopScope(
onWillPop: onWillPop,
child: yourPageClass(),
),
);
}
}
The snackbar will appear for 2000 milliseconds and during that time if the user press the back button again, the application will exit
Note to call ExitController class in main.dart, and wrapping your all structure(navigation class/page class/etc) with this ExitController class.
Example:
main.dart
import 'package:flutter/material.dart';
import 'package:app_name/structure.dart';
void main(){
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: ExitController(),
);
}
}
WillPopScope(
onWillPop: () async {
return await showDialog(context: ctx, builder: (_) => exitAlertDialog(_));
},
child: GestureDetector(onTap: () => FocusScope.of(ctx).unfocus(), child: widget));
Widget exitAlertDialog(BuildContext context) {
return AlertDialog(
backgroundColor: plColor2,
content: Text('Are you sure you want to exit?'),
actions: <Widget>[
TextButton(child: Text('No'), onPressed: () => Navigator.of(context).pop(false)),
TextButton(child: Text('Yes, exit'), onPressed: () => SystemNavigator.pop())
],
);
}
or you can use showsnackbar as your need
I have a button that displays a SnackBar (or toast) before moving to the next page. I have a countdown and after 5 seconds I push Page2.
RaisedButton(
onPressed: () {
_startTimer();
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(
'Prepare yourself to start in ${widget._current.toString()}!'), // doesn't work here
duration: new Duration(seconds: widget._start),
action: SnackBarAction(
label: widget._current.toString(), // and neither does here
onPressed: () {
// Some code to undo the change.
},
),
);
Scaffold.of(context).showSnackBar(snackBar);
},
child: Text(
"I'm ready",
style: TextStyle(fontSize: 20),
),
),
Nothing to see on the countdown but I'll paste it just in case:
void _startTimer() {
CountdownTimer countDownTimer = new CountdownTimer(
new Duration(seconds: widget._start),
new Duration(seconds: 1),
);
var sub = countDownTimer.listen(null);
sub.onData((duration) {
setState(() {
widget._current = widget._start - duration.elapsed.inSeconds;
});
});
sub.onDone(() {
print("Done");
sub.cancel();
});
}
So if I display the countdown somewhere else (inside a Text for example) it works but it seems that the SnackBar doesn't change its contain, it always get the max number of the countdown.
you need to implement a custom widget with countdown logic in side for the content field of snackbar, like this:
class TextWithCountdown extends StatefulWidget {
final String text;
final int countValue;
final VoidCallback? onCountDown;
const TextWithCountdown({
Key? key,
required this.text,
required this.countValue,
this.onCountDown,
}) : super(key: key);
#override
_TextWithCountdownState createState() => _TextWithCountdownState();
}
class _TextWithCountdownState extends State<TextWithCountdown> {
late int count = widget.countValue;
late Timer timer;
#override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), _timerHandle);
}
#override
void dispose() {
timer.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container(
child: Text("[$count] " + widget.text),
);
}
void _timerHandle(Timer timer) {
setState(() {
count -= 1;
});
if (count <= 0) {
timer.cancel();
widget.onCountDown?.call();
}
}
}
That is because the snack bar is built once, when the button is clicked. When the state updates, it rebuilds the widget tree according to the changes. The snack bar initially isn't in the widget tree, so it doesn't update.
Try to use stack and show a snack bar, and then you should be able to manipulate it however you need.
Hope it helps.
Update SnackBar Content
SnackBar content can be updated/rebuilt while it's visible by making its content: widget dynamic.
In the code sample below we're using a ValueNotifier and ValueListenableBuilder to dynamically rebuild the content of the SnackBar whenever ValueNotifier is given a new value.
(There are many ways to to maintain state values & rebuild widgets when it changes, such as RiverPod, GetX, StreamBuilder, etc. This example uses the Flutter native ValueListenableBuilder.)
When running this Flutter page, click the FAB to show the SnackBar, then click on the center text to update the SnackBar's content (multiple times if you like).
Example
Use SnackBarUpdateExample() widget in your MaterialApp home: argument to try this example in an emulator or device.
class SimpleCount {
int count = 0;
}
class SnackBarUpdateExample extends StatelessWidget {
static const _initText = 'Initial text here';
/// This can be "listened" for changes to trigger rebuilds
final ValueNotifier<String> snackMsg = ValueNotifier(_initText);
final SimpleCount sc = SimpleCount();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SnackBar Update'),
),
/// ↓ Nested Scaffold not necessary, just prevents FAB being pushed up by SnackBar
body: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Click FAB to show SnackBar'),
SizedBox(height: 20,),
Text('Then...'),
SizedBox(height: 20,),
InkWell(
child: Text('Click → here ← to update SnackBar'),
onTap: () {
sc.count++;
snackMsg.value = "Hey! It changed! ${sc.count}";
},
), /// When snackMsg.value changes, the ValueListenableBuilder
/// watching this value, will call its builder function again,
/// and update its SnackBar content widget
],
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.play_arrow),
onPressed: () => _showSnackBar(context),
),
),
);
}
void _showSnackBar(BuildContext context) {
var _controller = ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: SnackContent(snackMsg))
);
/// This resets the snackBar content when it's dismissed
_controller.closed.whenComplete(() {
snackMsg.value = _initText;
sc.count = 0;
});
}
}
/// The ValueListenableBuilder rebuilds whenever [snackMsg] changes.
class SnackContent extends StatelessWidget {
final ValueNotifier<String> snackMsg;
SnackContent(this.snackMsg);
#override
Widget build(BuildContext context) {
/// ValueListenableBuilder rebuilds whenever snackMsg value changes.
/// i.e. this "listens" to changes of ValueNotifier "snackMsg".
/// "msg" in builder below is the value of "snackMsg" ValueNotifier.
/// We don't use the other builder args for this example so they are
/// set to _ & __ just for readability.
return ValueListenableBuilder<String>(
valueListenable: snackMsg,
builder: (_, msg, __) => Text(msg));
}
}
I am trying to receive in my home the total lines that exist in my database. When I run the app for the first time in my text widget appears "null", it only changes if I go to another page and go back and the value is not updated. I add another line to bd and when I go back the value goes from null to 28 for example but there is already +1 line.
In home I am getting the value inside initState with setState. Any suggestion? Thank you!!
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
ContactHelper helper = ContactHelper();
List<Contact> listaSigilos = List();
int count;
#override
initState() {
super.initState();
setState(() {
helper.getAllContacts().then((list) {
listaSigilos = list;
count = listaSigilos.length;
print(list);
print("count: $count");
});
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.black,
onPressed: () {
Navigator.push(
context, MaterialPageRoute(builder: (context) => NovoSigilo()));
},
child: Icon(Icons.add),
),
appBar: AppBar(
title: Text("Stuff to do"),
backgroundColor: Colors.black,
centerTitle: true,
actions: <Widget>[],
),
body: Container(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("Active"),
Text("$count",//Show row count here
...
First of all never use setState() directly inside initState() method. Second, you were only listening to the database inside initState() which gets called just once, I have created a new method called _fetchData(), which is put inside initState() as well as one you come back from your second page.
#override
initState() {
super.initState();
_fetchData(); // fetch data in the start
}
void _fetchData() {
helper.getAllContacts().then((list) {
listaSigilos = list;
count = listaSigilos.length;
print(list);
print("count: $count");
setState(() {});
});
}
Update your floatingActionButton to
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.black,
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) => NovoSigilo())).then((value) {
_fetchData(); // fetch data after coming back to this page
});
},
child: Icon(Icons.add),
)
I'm trying to get the current snack bar (showing one) to determine if it is the same snack that am trying to show or not ; in other words ,i don't want to duplicate the snack , but i couldn't get it.
I'v tried to google it up but nothing showed about it and what is really pissing me off is when i call Scaffold.of(context).showSnackBar(sb); multiple times they show will show one at a time until they end .
import 'package:flutter/material.dart';
const sb = SnackBar(
content: Text('the snack bar'),
);
void main() => runApp(MaterialApp(
title: 'Snack bar test',
home: Scaffold(
appBar: AppBar(
title: Text('snack bar demo'),
),
body: Center(
child: MyApp(),
),
),
));
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return RaisedButton(
child: Text('push to test'),
onPressed: () {
// Scaffold.of(context).
Scaffold.of(context).showSnackBar(sb);
},
);
}
}
What i want to achieve is something like this :-
if (sb.isShowing()){
//hide or remove it
}
and thanks in advance .
You can use .close() event. It specifies how a snack bar is closed. Details here and Sample code below :
bool _isSnackbarActive = false ;
...
...
_isSnackbarActive = true ;
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("Title")))
.closed
.then((SnackBarClosedReason reason) {
// snackbar is now closed.
_isSnackbarActive = false ;
});
The variable _isSnackbarActive to true when snackBar is displayed and set it to false when it is closed. In your onPressed event, just check the status of the snackbar and decide if new snackbar is to be shown or not. In addition, per your requirement, you can check the text on the current snack bar to see if the intended snackbar and the current one are same.
To me this works fine, and don't need an external variable to control visibility status. Maybe would be helpful for others.
I think it isn't the best approach, because it will not prevent new SnackBars, it will just clean the "queue" on close.
ScaffoldMessenger.of(context)
.showSnackBar(_snackBar)
.closed
.then((value) => ScaffoldMessenger.of(context).clearSnackBars());
Use ScaffoldMessenger.of(context).removeCurrentSnackBar(); since Scaffold.of(context).removeCurrentSnackBar(); is deprecated.
Just call removeCurrentSnackBar() to remove the current snackbar.
import 'package:flutter/material.dart';
const sb = SnackBar(
content: Text('the snack bar'),
);
void main() => runApp(MaterialApp(
title: 'Snack bar test',
home: Scaffold(
appBar: AppBar(
title: Text('snack bar demo'),
),
body: Center(
child: MyApp(),
),
),
));
class MyApp extends StatelessWidget {
const MyApp({Key key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('push to test'),
onPressed: () {
// Scaffold.of(context).
Scaffold.of(context).showSnackBar(sb);
},
),
RaisedButton(
child: Text('Remove snackbar'),
onPressed: () {
// Scaffold.of(context).
Scaffold.of(context).removeCurrentSnackBar();
},
),
],
);
}
}
Adding to what #Sukhi mentioned
//Set this in state
...
bool _isSnackbarActive = false;
...
.
.
.
//If you want to show snackBar on a button press
onPressed: () {
if (!_isSnackbarActive) {
showSnack();
}
//This is the showSnack(). When called it'll set the _isSnackbarActive to true and now even if you click on the button multiple times since it's true a new snackBar will not be shown. Once the current snackBar closes it set _isSnackbarActive to false and then it can be called again.
showSnack() {
setState(() {
_isSnackbarActive = true;
});
return ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Posts updated!")))
.closed
.then((SnackBarClosedReason reason) {
// snackbar is now closed.
setState(() {
_isSnackbarActive = false;
});
});
}
There are two easy ways:
removeCurrentSnackBar will disappear without animation
ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("snackbar text")));
hideCurrentSnackBar will disappear with animation
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("snackbar text")));
chose according to your design.
I am learning app development on Flutter and can't get my Slider to work within the AlertDialog. It won't change it's value.
I did search the problem and came across this post on StackOverFlow:
Flutter - Why slider doesn't update in AlertDialog?
I read it and have kind of understood it. The accepted answer says that:
The problem is, dialogs are not built inside build method. They are on a different widget tree. So when the dialog creator updates, the dialog won't.
However I am not able to understand how exactly does it have to be implemented as not enough background code is provided.
This is what my current implementation looks like:
double _fontSize = 1.0;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(qt.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.format_size),
onPressed: () {
getFontSize(context);
},
),
],
),
body: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 15.0),
itemCount: 3,
itemBuilder: (context, index) {
if (index == 0) {
return _getListTile(qt.scripture, qt.reading);
} else if (index == 1) {
return _getListTile('Reflection:', qt.reflection);
} else {
return _getListTile('Prayer:', qt.prayer);
}
})
);
}
void getFontSize(BuildContext context) {
showDialog(context: context,builder: (context){
return AlertDialog(
title: Text("Font Size"),
content: Slider(
value: _fontSize,
min: 0,
max: 100,
divisions: 5,
onChanged: (value){
setState(() {
_fontSize = value;
});
},
),
actions: <Widget>[
RaisedButton(
child: Text("Done"),
onPressed: (){},
)
],
);
});
}
Widget parseLargeText(String text) {...}
Widget _getListTile(String title, String subtitle) {...}
I understand that I will need to make use of async and await and Future. But I am not able to understand how exactly. I've spent more than an hour on this problem and can't any more. Please forgive me if this question is stupid and noobish. But trust me, I tried my best.
Here is a minimal runnable example. Key points:
The dialog is a stateful widget that stores the current value in its State. This is important because dialogs are technically separate "pages" on your app, inserted higher up in the hierarchy
Navigator.pop(...) to close the dialog and return the result
Usage of async/await
import 'package:flutter/material.dart';
void main() => runApp(App());
class App extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
double _fontSize = 20.0;
void _showFontSizePickerDialog() async {
// <-- note the async keyword here
// this will contain the result from Navigator.pop(context, result)
final selectedFontSize = await showDialog<double>(
context: context,
builder: (context) => FontSizePickerDialog(initialFontSize: _fontSize),
);
// execution of this code continues when the dialog was closed (popped)
// note that the result can also be null, so check it
// (back button or pressed outside of the dialog)
if (selectedFontSize != null) {
setState(() {
_fontSize = selectedFontSize;
});
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Font Size: ${_fontSize}'),
RaisedButton(
onPressed: _showFontSizePickerDialog,
child: Text('Select Font Size'),
)
],
),
),
);
}
}
// move the dialog into it's own stateful widget.
// It's completely independent from your page
// this is good practice
class FontSizePickerDialog extends StatefulWidget {
/// initial selection for the slider
final double initialFontSize;
const FontSizePickerDialog({Key key, this.initialFontSize}) : super(key: key);
#override
_FontSizePickerDialogState createState() => _FontSizePickerDialogState();
}
class _FontSizePickerDialogState extends State<FontSizePickerDialog> {
/// current selection of the slider
double _fontSize;
#override
void initState() {
super.initState();
_fontSize = widget.initialFontSize;
}
#override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Font Size'),
content: Container(
child: Slider(
value: _fontSize,
min: 10,
max: 100,
divisions: 9,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
),
actions: <Widget>[
FlatButton(
onPressed: () {
// Use the second argument of Navigator.pop(...) to pass
// back a result to the page that opened the dialog
Navigator.pop(context, _fontSize);
},
child: Text('DONE'),
)
],
);
}
}
You just need to warp the AlertDialog() with a StatefulBuilder()