Update SnackBar content in Flutter - flutter

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));
}
}

Related

It is possible to preserve state of PopUpMenuButton?

Currently i am working on music app and according to my ui i have to display download, downloading progress and downloaded status shown inside popup menu item.But according to Popup menu button widget behaviour, it is dispose and unmounted.So when i closed popup menu item and again open the last status always display download instead of downloading.So it is possible to prevent popup menu button after close.
I tried callback functions, provider, getx, auto keep alive and also stateful builder but it is not working.
I am using ValueNotifier to preserve the download progress. To preserve the state you can follow this structure and use state-management property like riverpod/bloc
class DTest extends StatefulWidget {
const DTest({super.key});
#override
State<DTest> createState() => _DTestState();
}
class _DTestState extends State<DTest> {
/// some state-management , also can be add a listener
ValueNotifier<double?> downloadProgress = ValueNotifier(null);
Timer? timer;
_startDownload() {
timer ??= Timer.periodic(
Duration(milliseconds: 10),
(timer) {
downloadProgress.value = (downloadProgress.value ?? 0) + .01;
if (downloadProgress.value! > 1) timer.cancel();
},
);
}
#override
void dispose() {
timer?.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
PopupMenuButton(
itemBuilder: (context) {
return [
PopupMenuItem(
child: ValueListenableBuilder(
valueListenable: downloadProgress,
builder: (context, value, child) => InkWell(
onTap: value == null ? _startDownload : null,
child: Text("${value ?? "Download"}")),
),
)
];
},
)
],
),
);
}
}

Flutter Keyboard show and immediately disappeared at textfied in listview

I have a list view and want to edit the Tile's title. When user click the edit icon, text widget change to TextField. Once user tap the textfield, keyboard show and immediately disappeared.
May I know what is the issue?
class EditableListTile extends StatefulWidget {
final Favourite favourite;
final Function onChanged;
final Function onTap;
const EditableListTile(
{Key? key,
required this.favourite,
required this.onChanged,
required this.onTap})
: super(key: key);
#override
_EditableListTileState createState() => _EditableListTileState();
}
class _EditableListTileState extends State<EditableListTile> {
Favourite? favourite;
late bool _isEditingMode;
late TextEditingController _titleEditingController;
#override
void initState() {
super.initState();
favourite = widget.favourite;
_isEditingMode = false;
}
#override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
widget.onTap(favourite);
},
leading: leadingWidget,
title: titleWidget,
trailing: tralingButton,
);
}
Widget get leadingWidget {
return SizedBox(
width: 32,
child: FolderIcon(
color: Theme.of(context).iconTheme.color!,
),
);
}
Widget get titleWidget {
if (_isEditingMode) {
_titleEditingController = TextEditingController(text: favourite?.name);
return TextField(
controller: _titleEditingController,
);
} else {
return Text(favourite!.name);
}
}
Widget get tralingButton {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
(favourite?.isDefault == false)
? (_isEditingMode
? IconButton(
icon: const Icon(Icons.check),
onPressed: saveChange,
)
: IconButton(
icon: const Icon(Icons.edit),
onPressed: _toggleMode,
))
: Container(),
_isEditingMode
? IconButton(
icon: const Icon(Icons.cancel_outlined),
onPressed: cancelChange,
)
: Container()
],
);
}
void _toggleMode() {
setState(() {
_isEditingMode = !_isEditingMode;
});
}
void cancelChange() {
setState(() {
_isEditingMode = !_isEditingMode;
});
}
void saveChange() {
favourite!.name = _titleEditingController.text;
_toggleMode();
widget.onChanged(favourite!);
}
}
you get this error because you initialized the TextEdittingController inside the titleWidget. every time the widget rebuild, create a new instance of TextEdittingController.
on top of your clas change it like this
// late TextEditingController _titleEditingController; <== Change This
TextEditingController _titleEditingController = TextEditingController();
in titleWidget change your code to this.
Widget get titleWidget {
if (_isEditingMode) {
_titleEditingController.text = favourite?.name;
Real culprit is key in ListView.seperate. I used key : UniqueKey(). If I change to ValueKey(state.favourites[index]), it is working now.
I used key : UniqueKey() because I have onDismissed but one of the item, want to trigger onDismissed but don't want to dismissed.
Let's say, item is folder name and if user delete the item, delete the folder and delete all the files under that folder. But one folder is system generated and don't want user to delete that folder but let them to delete files. So we call confirmDismiss and tell the user that it is system generated folder and only will delete files.
But list view don't allow. So I found out the UniqueKey is work
around. So edit is importance. So that I take out UniqueKey.
Like Alex Aung's answer above, they're right that the use of UniqueKey() is a problem .
In my case I had an action button that pushed a page route to the navigator. On the content view (GameView) I had a UniqueKey() set and it was responsible for series of issues with input fields downstream.
Any time I set this back to UniqueKey(), any clicking inside a downstream TextFormField causes the Keyboard open then immediately close.
Widget getActionButton() {
return FloatingActionButton(
onPressed: () {
final Account account = Account("test#test.com");
widget.viewModels.gamesModel.load(account);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GamesView(
key: Key("GameView"), // UniqueKey() is a problem here.
account: account,
viewModels: widget.viewModels,
)
)
);
},
tooltip: 'Save Changes',
child: Icon(Icons.save),
);
}

How to dismiss an open dropdown in Flutter?

I've a dropdown on my screen, which I want to close every time the user minimises the app (Pause State). I looked into the DropDownButton widget, and it seems it uses Navigator.push() method, so ideally it should be dismissed with Navigator.pop(), if I'm not wrong.
Here is what I've tried already -
Use global key to get the context of dropdown widget and implement Navigator.pop(key.currentContext) inside didChangeAppLifecycleState() function.
Use focusNode to implement .unfocus() to remove the focus from the dropdown.
Maintain a isDropdownDialogOpen boolean which I set to true on onTap() and false on onChange(). And then simple pop() if its true on app minimisation. But this approach fails when the user opens the dropdown and then closes it by tapping outside the dropdown dialog. I can't set the boolean to false in that case.
My requirement is that - I've to dismiss the dropdown whenever the user minimises the app, if it was open in the first place.
I've gone through bunch of SO questions and GitHub comments, that are even remotely similar, but couldn't find anything helpful.
Loose Code -
class Auth extends State<Authentication> with WidgetsBindingObserver {
bool isDropdownOpen = false;
GlobalKey _dropdownKey = GlobalKey();
FocusNode _focusNode;
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// To pop the open dropdown dialog whenever app is minimised (and opened back on again)
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
if(isDropdownOpen)
pop();
// Also tried pop(_dropdownKey.currentContext)
break;
case AppLifecycleState.paused:
break;
case AppLifecycleState.inactive:
break;
case AppLifecycleState.detached:
break;
}
}
build() {
return Scaffold(
// Dummy Dropdown button with random values
body: DropdownButton(
key: _dropdownKey,
focusNode: _focusNode,
value: "one",
items: ["one", "two", "three", "four", "five"],
//boolean values changing fine, but when user taps outside the dropdown dialog, dropdown is closed and neither of onChanged() and onTap() is called. Need a call back (or something similar) for this particular case.
onChange(value) => isDropdownOpen = false;
onTap() => isDropdownOpen = ! isDropdownOpen;
)
);
}
}
For your ref - https://github.com/flutter/flutter/issues/87989
Does this work for you?
import 'package:flutter/material.dart';
void main() async {
runApp(
MyApp(),
);
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({Key key}) : super(key: key);
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> with WidgetsBindingObserver {
#override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
AppLifecycleState currentAppLifecycleState;
bool isDropdownMenuShown = false;
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
setState(
() => currentAppLifecycleState = state,
);
}
String value;
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (<AppLifecycleState>[
AppLifecycleState.inactive,
AppLifecycleState.detached,
AppLifecycleState.paused,
].contains(currentAppLifecycleState) &&
isDropdownMenuShown) {
print('Closing dropdown');
Navigator.pop(context);
}
});
return Scaffold(
appBar: AppBar(
title: const Text('Flutter Demo'),
),
body: Center(
child: GestureDetector(
onLongPressStart: (LongPressStartDetails details) async {
final left = details.globalPosition.dx;
final top = details.globalPosition.dy;
setState(() => isDropdownMenuShown = true);
final value = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(left, top, left, top),
items: <PopupMenuEntry<String>>[
PopupMenuItem(
child: const Text('Option 1'),
value: 'Option 1',
),
PopupMenuItem(
child: const Text('Option 2'),
value: 'Option 2',
),
PopupMenuItem(
child: const Text('Option 3'),
value: 'Option 3',
),
],
);
setState(() => isDropdownMenuShown = false);
if (value == null) return;
print('You chose: $value');
},
child: InkWell(
onTap: () {},
child: Container(
alignment: Alignment.center,
child: Text(
'Long press to show dropdown',
textAlign: TextAlign.center,
),
),
),
),
),
);
}
}
How does it work?
We keep track of the current app state using the didChangeAppLifecycleState method.
Whenever the dropdown is shown the isDropdownMenuShown variable is set to true.
Whenever the dropdown is closed/exited the variable is set to false.
We add a post frame callback (it is called after each rebuild) and we check if the app went into background and if the dropdown is shown. If yes, the dropdown is closed.
You could use FocusManager.instance.primaryFocus?.unfocus();

Flutter-web TextFormField issue

usually I can disable/grey-out a button until a TextFormField meets certain parameters in flutter by something like this:
TextFormField(
controller: _controller
value: (value)
)
SubmitButton(
onPressed: _controller.text.isNotEmpty ? _submit : null;
)
But when compiled as a website the Button seems no longer aware of the controller value...
I have tried targeting in several different ways, e.g. _controller.value.text.isEmpty and _controller.text.isEmpty...
I'm guessing I'm missing something or this method just isn't possible for web ... Is there any other way to get the same result?
To be honest, your code shouldn't work in flutter mobile either, but may be works because of screen keyboard causes widget rebuild when showing or hiding.
To fix this issue we have to use stateful widget with state variable like canSubmit and update it in textField's listener onChange with setState method. Then every time the text changes, our stateful widget will update the submit button..
class Page extends StatefulWidget {
#override
_PageState createState() => _PageState();
}
class _PageState extends State<Page> {
bool canSubmit;
#override
void initState() {
canSubmit = false;
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
onChanged: (value) {
setState(() {
canSubmit = value.isNotEmpty;
});
},
),
RaisedButton(
onPressed: canSubmit ? _submit : null,
child: Text('Submit'),
)
],
),
),
);
}
void _submit() {
print('Submitted');
}
}

How do i change a boolean in a StatefullWidget from another one?

i'm brand new to Flutter.
I'm trying to open a panel pressing a button and than closing it by pressing a button on that panel.
I've managed to do it by writing the code in the same page.
What i can't do is splitting the code and keep everything working.
What I'm actually doing is calling a variable in the State of a widget that is initialized False and then with an if statement i'm calling: or an empty container or the panel i want.
When i press the button i call SetState(){} and the variable changes to true to let the panel appears, then in the panel there's button that do opposite thing.
Assuming that what i'm doing it is correct. How to i keep doing this with the panel refactored in a new page?
I've red something about streams and inherited widgets but i haven't completely understood
If I understand correctly, you want to notify a StatefullWidget from another StatefullWidget. There are several approaches on this one but since you've mentioned Streams, I'll try to post an example and explain a bit this scenario.
So basically, you can consider the streams like a pipe linked to a faucet in one end and the other end it's added into a cup (the end can be split in multiple ends and put in multiple cups, "broadcast streams").
Now, the cup is the listener (subscriber) and waits for water to drop trough the pipe.
The faucet is the emitter, and it will emit water droplets when the faucet is opened.
The faucet can be opened when the other end is put into a cup, this is a smart faucet with some cool sensors, (the emitter will start emitting events when a subscriber is "detected).
The droplets are actual events that are happening in the the app.
Also you must remember to close the faucet in order to avoid a massive leak from your cup into the kitchen floor. (you must cancel the subscribers when you've done handling events to avoid a leak).
Now for your particular case here's the code snippet that kind of illustrate the above metaphor:
class ThePannel extends StatefulWidget { // this is the cup
final Stream<bool> closeMeStream; // this is the pipe
const ThePannel({Key key, this.closeMeStream}) : super(key: key);
#override
_ThePannelState createState() => _ThePannelState(closeMeStream);
}
class _ThePannelState extends State<ThePannel> {
bool _closeMe = false;
final Stream<bool> closeMeStream;
StreamSubscription _streamSubscription;
_ThePannelState(this.closeMeStream);
#override
void initState() {
super.initState();
_streamSubscription = closeMeStream.listen((shouldClose) { // here we listen for new events coming down the pipe
setState(() {
_closeMe = shouldClose; // we got a new "droplet"
});
});
}
#override
void dispose() {
_streamSubscription.cancel(); // THIS IS QUITE IMPORTANT, we have to close the faucet
super.dispose();
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
SomeWidgetHere(shouldClose: _closeMe),
RaisedButton(
onPressed: () {
setState(() {
_closeMe = true;
});
},
)
],
);
}
}
class SomeWidgetThatUseThePreviousOne extends StatefulWidget { // this one is the faucet, it will emit droplets
#override
_SomeWidgetThatUseThePreviousOneState createState() =>
_SomeWidgetThatUseThePreviousOneState();
}
class _SomeWidgetThatUseThePreviousOneState
extends State<SomeWidgetThatUseThePreviousOne> {
final StreamController<bool> thisStreamWillEmitEvents = StreamController(); // this is the end of the pipe linked to the faucet
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ThePannel(closeMeStream: thisStreamWillEmitEvents.stream), // we send the other end of the pipe to the cup
RaisedButton(
child: Text("THIS SHOULD CLOSE THE PANNEL"),
onPressed: () {
thisStreamWillEmitEvents.add(true); // we will emit one droplet here
},
),
RaisedButton(
child: Text("THIS SHOULD OPEN THE PANNEL"),
onPressed: () {
thisStreamWillEmitEvents.add(false); // we will emit another droplet here
},
)
],
);
}
#override
void dispose() {
thisStreamWillEmitEvents.close(); // close the faucet from this end.
super.dispose();
}
}
I hope that my analogy will help you understand a bit the streams concept.
If you want to open an dialog (instead of what you call a "panel") you can simply give the selected data back when you close the dialog again.
You can find a good tutorial here: https://medium.com/#nils.backe/flutter-alert-dialogs-9b0bb9b01d28
you can navigate and return data from another screen like that :
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
title: 'Returning Data',
home: HomeScreen(),
));
}
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Returning Data Demo'),
),
body: Center(child: SelectionButton()),
);
}
}
class SelectionButton extends StatelessWidget {
#override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: Text('Pick an option, any option!'),
);
}
// A method that launches the SelectionScreen and awaits the result from
// Navigator.pop!
_navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push returns a Future that will complete after we call
// Navigator.pop on the Selection Screen!
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
// After the Selection Screen returns a result, hide any previous snackbars
// and show the new result!
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("$result")));
}
}
class SelectionScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
// Close the screen and return "Yep!" as the result
Navigator.pop(context, 'Yep!');
},
child: Text('Yep!'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
// Close the screen and return "Nope!" as the result
Navigator.pop(context, 'Nope.');
},
child: Text('Nope.'),
),
)
],
),
),
);
}
}
for more details about navigation:
https://flutter.dev/docs/cookbook/navigation/returning-data