How to change the state of an Action widget in an AlertDialog - flutter

I want to validate the user input and then enable the Dialog's Submit action.
Using StatefulBuilder inside the dialog's content doesn't work here, since the actions does not live in the content Widget. So how would one go about changing a property on an action widget?
Currently my workaround is to not use actions, and have a Row of buttons inside the dialog's content, which is OK, but I'd like to know whether there is a better way.
I feel that using a Provider just to hold the "validation result" is a bit overkill.
EDIT: Example demonstrating that a StatefulBuilder allows only inner widgets to be updted with state changes. The button sets enabled, but the of two "action buttons" only the one inside the StatefulBuilder is rebuilt. The AlertDialog actions does not get updated.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Dialogs'),
),
body: Center(
child: TextButton(
child: Text('Clickme'),
onPressed: _dialogWithAction,
),
),
);
}
Future<void> _dialogWithAction() async {
showDialog<bool>(
context: context,
builder: (BuildContext dialogContext) {
bool enabled = false;
return AlertDialog(
title: Text('Add Address'),
content: StatefulBuilder(builder: (context, innerSetState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
child: Text('Enable Submit'),
onPressed: () {
enabled = true;
innerSetState(() {});
},
),
Row(
children: [
// This Workaround widget is rebuild by the stateSetter
TextButton(
onPressed: enabled ? () => Navigator.pop(context) : null,
child: Text('Submit'),
)
],
)
],
);
}),
actions: <Widget>[
// This Widget does not get rebuilt when "enabled" is changed
TextButton(
child: Text('Submit'),
onPressed: enabled
? () {
Navigator.of(dialogContext).pop();
}
: null,
),
],
);
},
);
}
}

Related

How to add multiple children to flutter body

Good day, I need help adding a second child to the body of my screen. I keep on getting the error "The argument for the named parameter 'children' was already specified." If I take that piece of code out, my 'app' works perfectly. I've tried adding Column to my body (saw it in a different question) but it still gives me the error.
The problematic code is
,children: [TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const FeatureScreenDos(title: 'Feature Screen dos');
Full Code:
class DashBoard extends StatelessWidget {
const DashBoard({Key? key, required this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const FeatureScreenUno(title: 'Feature Screen uno');
}));
},
child: const Text('Feature Screen uno')
)
]
,children: [TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const FeatureScreenDos(title: 'Feature Screen dos');
}));
}
,child: const Text('Feature Screen dos'),
),
]
)
);
}
}`
adding multiple children to Column or Row or any other multi child widget is not like that,
you would have a single Column with a list of children inside it
Column(
children: [
const Text('Child 1'),
const Text('Child 2'),
...
]
)
please refer to Column widget to know more about column, same work goes to row & some other multi child widgets
class DashBoard extends StatelessWidget {
const DashBoard({Key? key, required this.title}) : super(key: key);
final String title;
#override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Column(
children: [
///child one
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const FeatureScreenUno(title: 'Feature Screen uno');
}));
},
child: const Text('Feature Screen uno')
),
///child two
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (context) {
return const FeatureScreenDos(title: 'Feature Screen dos');
}));
}
,child: const Text('Feature Screen dos'),
),
]
)
);
}
}`
body will always take one child we customize further ourselfs by adding column and rows for example here you can add a row as well in children if you want to show something horizontal after text buttons and further add more children to rows

How to call function from Stateful widget

I have seen many questions similar to mine in Stack Overflow but it did not fit my case since they were asking to call function from - to Stateful widget.
I want call function located into State Full widget from a non Stateful-Stateless Widget
My code is complicated, I will try to explain it below:
class Example extends StatefulWidget {
const Example({Key? key}) : super(key: key);
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
void myFunction(){
print('hello dart');
}
ShowDialog showDialog = ShowDialog();
#override
Widget build(BuildContext context) {
return TextButton(
onPressed: (){
showDialog.myDialog();
},
child: Text('tab me')
);
}
}
class ShowDialog {
Widget myDialog(){
return showDialog(
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: () {
// here i need to call myFunction() Methood
},
child: const Text("tab")
),
],
)
},
);
}
}
How can I go through this?
you can call it directly like this:
_ExampleState().myFunction();
The full code:
class Example extends StatefulWidget {
const Example ({Key? key, required this.title}) : super(key: key);
final String title;
#override
State<Example > createState() => _ExampleState ();
}
class _ExampleState extends State<Example > {
void myFunction(){
print('hello dart');
}
ShowDialog showDialog = ShowDialog();
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: TextButton(
onPressed: (){
showDialog.myDialog(context);
},
child: Text('tab me')
)
);
}
}
class ShowDialog {
Future myDialog(BuildContext context){
return showDialog(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: () {
// here i need to call myFunction() Methood
_ExampleState().myFunction();
},
child: const Text("tab")
),
],
);
},
);
}
}
The result:
So in order to have your showDialog function call MyFunction, you need to pass it as if it was a callback.
To do that, first add a Function callback in your class:
class ShowDialog {
ShowDialog({required this.callback});
VoidCallback callback;
...
}
Then you have to pass the callback when you create the object:
ShowDialog showDialog = ShowDialog(callback: myFunction);
You actually can't do that tho, because this is a class variable, a simple solution is to turn your showDialog variable into a getter, this means showDialog will compute again every time you call it, which is not ideal but I don't think it will be terrible for this specific use-case.
ShowDialog get showDialog => ShowDialog(callback: myFunction);
note the get keyword and the => instead of an equal sign
EDIT:
You can also pass the callback as part of the myDialog method, this is probably actually a better idea:
Widget myDialog(VoidCallback callback) {
return showDialog(
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: callback,
child: const Text("tab")
),
],
)
},
);
}
I wrote this function that returns a Dialog in a separate file :
Future myDialog({required BuildContext dialogContext, required Function function}){
return showDialog(
context: dialogContext,
builder: (BuildContext context) {
return SimpleDialog(
backgroundColor: Colors.deepPurple[900],
titleTextStyle: const TextStyle(
color: Colors.red, fontSize: 18),
children: [
ElevatedButton(
onPressed: () {
function();
},
child: const Text("tab")
),
],
);
},
);
}
then I made 2 functions, the first one just print (Hello first function) and the second function print (Hello Second function) and set the state and rebuild the widget tree
I made 2 TextButton: the first TextButton call the myDialog function and pass the firstFunction as a parameter, the second TextButton call the myDialog function and pass the secondFunction as a parameter :
the code :
class ExampleState extends State<Example > {
void firstFunction(){
print('Hello first function');
}
void secondFunction(){
setState(() {
print('Hello Second function');
});
}
#override
Widget build(BuildContext context) {
print('build');
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
children: [
TextButton(
onPressed: (){
myDialog(dialogContext: context, function: firstFunction );
},
child: Text('First Dialog')
),
TextButton(
onPressed: (){
myDialog(dialogContext: context, function: secondFunction );
},
child: Text('Second Dialog')
)
],
),
)
);
}
}
notice that I added a print('build'); so I can know when it rebuild the widget tree
The result:

How to 'setstate' an element from one AlertDialog

I made two forms with pass fields on both, then made all the code to, when the user clicks in the eye Icon, the field show the password, clicking again it hide the password.
But now I put these forms inside an Alert Dialog widget and now it doesn't updating when I click in the icon, only updates if I close the dialog and open again (you open the dialog, click in the icon, it doesn't change. If you close and open again you see the icon changed)
After some search I tried Stateful Builder but it doesn't work too.
Dialog:
Future<void> _myDialog(child){
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return SingleChildScrollView(
child: AlertDialog(
content: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Padding(
padding: EdgeInsets.all(20),
child: child,
);
},
),
insetPadding: EdgeInsets.only(left: 10, right: 10),
),
);
}
);
}
Toggle method referenced in my textFields:
void _toggle(int index) {
setState(() {
_toggleList[index] = !_toggleList[index];
});
}
How can I toggle it instantly when the user click in the icon as outside the alert?
Edit
Row _showButtons(){
return Row(
children: [
RaisedButton(
child: Text("Change email"),
onPressed: () {_myDialog(_showEmailFields());}
),
RaisedButton(
child: Text("Change pass"),
onPressed: () {_myDialog(_showPassFields());}
),
],
);
}
I have created a structure for you that you should use for achieving what you want here. Make your forms into a separate stateful widgets, so that they have their own State, and you call the right setState function. Right now the setState function you are calling does not belongs to the state of your alert dialog.
class Test extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestState();
}
class _TestState extends State<Test> {
void showForm() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("Login"),
content: LoginWidget(),
);
},
);
}
#override
Widget build(BuildContext context) {
TextTheme textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(
title: Text("Appbar"),
),
body: Center(
child: RaisedButton(
child: Text("Show Form"),
onPressed: showForm,
),
),
);
}
}
class LoginWidget extends StatefulWidget {
LoginWidget({Key key}) : super(key: key);
#override
LoginWidgetState createState() => LoginWidgetState();
}
class LoginWidgetState extends State<LoginWidget> {
GlobalKey<FormState> _formKey;
bool _passwordVisible;
#override
void initState() {
super.initState();
_formKey = GlobalKey<FormState>();
_passwordVisible = false;
}
#override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: InputDecoration(labelText: "Email"),
),
SizedBox(
height: 10,
),
TextFormField(
obscureText: !_passwordVisible,
decoration: InputDecoration(
labelText: "Password",
suffixIcon: IconButton(
onPressed: () {
setState(() {
_passwordVisible = !_passwordVisible;
});
},
icon: Icon(
_passwordVisible ? Icons.visibility : Icons.visibility_off),
),
),
),
],
),
);
}
}

SwitchListTile not turning on in a Dialog

I am trying to create a workable switchlist in a Dialog. The Switch with icon, title shows but is not turning on.
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton.icon(
icon: Icon(AntDesign.sound),
label: Text('Sound Options'),
onPressed: () {
setState(() {
showDialog(
context: context,
child: SimpleDialog(
children: <Widget>[
SimpleDialogOption(
child: SwitchListTile(
title: Text('Mute'),
secondary: Icon(AntDesign.sound),
value: _mute,
onChanged: (bool value) {
setState(() {
_mute = value;
});
}),
),
],
));
});
},
)
],
),
),
),
);
}
setState rebuilds your build method and change the _mute value, but the SimpleDialog is open in another context, so that dialog is not rebuild. You should make a new StatefulWidget with the AlertDialog and setState when changing the value there
class Class1 extends StatefulWidget {
#override
_Class1State createState() => _Class1State();
}
class _Class1State extends State<Class1> {
bool _mute = false;
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
children: <Widget>[
FlatButton.icon(
icon: Icon(AntDesign.sound),
label: Text('Sound Options'),
onPressed: () {
showDialog(
context: context,
builder: (context) => MyDialog(_mute)
);
},
)
],
),
),
),
);
}
}
class MyDialog extends StatefulWidget {
bool mute;
MyDialog(this.mute);
#override
_MyDialogState createState() => _MyDialogState();
}
class _MyDialogState extends State<MyDialog> {
#override
Widget build(BuildContext context) {
return SimpleDialog(
children: <Widget>[
SimpleDialogOption(
child: SwitchListTile(
title: Text('Mute'),
secondary: Icon(AntDesign.sound),
value: widget.mute,
onChanged: (bool value) {
setState(() {
widget.mute = value;
});
}),
),
],
);
}
}
Another few tips, use builder instead of child with showDialog, child is deprecated, don't wrap the whole SimpleDialog in a setState, you should just wrap the value you're going to change, wrapping the dialog won't make it rebuild with the new value.
From the documentation
Calling setState notifies the framework that the internal state of
this object has changed in a way that might impact the user interface
in this subtree, which causes the framework to schedule a build for
this State object.
the setState you use rebuilds the subtree of your whole Scaffold Stateful Widget, but the Dialog is open in another tree, it's not part of that subtree, If you use Android Studio there is a flutter inspector (and I believe Visual Studio has it too), you can see there that the dialog and your StatefulWidget come from Material App (the dialog is not part of the subtree so it won't rebuild)

How to warn a user before closing the expansion tile by showing a popup message in flutter?

I'm learning about expansion tiles in flutter. Is there any way to show a pop up before closing the expansion tile? If ok is pressed close the expansion tile, if cancel is pressed retain the state of expansion tile. How can I do this?
ExpansionTile docs state that it has a property onExpansionChanged:
Called when the tile expands or collapses.
When the tile starts expanding, this function is called with the value true. When the tile starts collapsing, this function is called with the value false.
One way would be to combine this property (for example, whenever you receive false) with something like showDialog:
Displays a Material dialog above the current contents of the app, with Material entrance and exit animations, modal barrier color, and modal barrier behavior (dialog is dismissible with a tap on the barrier).
Edit - addressing edited question:
It seems that programatically expanding/collapsing ExpansionTile is somewhat problematic. There's an issue in Flutter repository for it.
However, I believe you could achieve what you seek by extending ExpansionTile as discussed in this answer. It is a little involved, though.
Another way to do that would be employing ExpansionPanelList and ExapnsionPanels, as suggested here. I've included a working (but crude) proof-of-concept app below. Feel free to modify and adapt it to your needs.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Expanding panels',
home: HomePage(),
);
}
}
class HomePage extends StatefulWidget {
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
bool expanded = true;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pushing data'),
),
body: Center(
child: Card(
child: Column(
children: <Widget>[
ExpansionPanelList(
expansionCallback: (int index, bool status) {
if (status == true) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: new Text('Alert Dialog'),
content: new Text('What do you want to do?'),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text(
'Collapse',
style: TextStyle(color: Colors.red),
),
onPressed: () {
setState(() {
expanded = false;
});
Navigator.of(context).pop();
},
),
],
);
},
);
} else {
setState(() {
expanded = true;
});
}
},
children: [
ExpansionPanel(
canTapOnHeader: true,
isExpanded: expanded,
headerBuilder: (BuildContext context, bool expanded) {
return Text('Expansion Panel Header');
},
body: Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Expanded panel body'),
),
),
),
],
),
],
),
),
),
);
}
}