Reputation: 83
I have a parent StatefulWidget with a StatelessWidget child, which returns an AlertDialog box. The StatelessWidget is built from a builder in the StatefulWidget when the "green download" button is pressed. (Upon confirmation in the AlertDialog the full code would then get and store the data).
Within the AlertDialog box is a DropdownButtonFormField. I've built in my own validation and error message to ensure the associated value is not null. (I couldn't get the built-in validation of the DropdownButtonFormField to show the whole error message without it being cut-off).
I can't understand why my AlertDialog isn't being updated to show the error message following the callback's SetState, even with a StatefulBuilder (which I might not be using correctly). I have tried using a StatefulWidget
Current Output: When you press the yes button in the AlertDialog, but the dropdown value is null or empty, the AlertDialog does not update to show the Centre widget in the AlertDialog that displays the error message. If you pop the AlertDialog and reopen it, it displays the error message.
Desired Output When you press the the yes button in the AlertDialog, but the dropdown value is null or empty, the AlertDialog updates to show the Centre widget in the AlertDialog that displays the error message.
Please can you help?
Useable code to recreate below:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isLoading = false;
bool _downloaded = false;
File cardImage;
String _languageDropdownValue;
bool isError = false;
List<Map<String, String>> _languages = [
{'code': 'en', 'value': 'English'},
{'code': 'fr', 'value': 'French'},
];
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: _downloaded
? IconButton(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 0),
icon: Icon(
Icons.open_in_new,
size: 45.0,
color: Colors.green,
),
onPressed: () {
print('Open button pressed');
})
: _isLoading
? CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
)
: IconButton(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 0),
icon: Icon(
Icons.download_rounded,
size: 45.0,
color: Colors.green,
),
onPressed: () {
print('Download button pressed');
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return DownloadScreen(
callbackFunction: alertDialogCallback,
dropDownFunction: alertDialogDropdown,
isError: isError,
languages: _languages,
languageDropdownValue: _languageDropdownValue,
);
});
},
);
}),
),
);
}
String alertDialogDropdown(String newValue) {
setState(() {
_languageDropdownValue = newValue;
});
return newValue;
}
alertDialogCallback() {
if (_languageDropdownValue == null || _languageDropdownValue.isEmpty) {
setState(() {
isError = true;
});
} else {
setState(() {
isError = false;
startDownload();
});
}
}
void startDownload() async {
print('selected language is: $_languageDropdownValue');
Navigator.pop(context);
print('start download');
setState(() => _downloaded = true);
}
}
class DownloadScreen extends StatelessWidget {
DownloadScreen(
{@required this.callbackFunction,
@required this.dropDownFunction,
@required this.isError,
@required this.languages,
@required this.languageDropdownValue});
final Function callbackFunction;
final Function dropDownFunction;
final String languageDropdownValue;
final bool isError;
final List<Map<String, String>> languages;
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: EdgeInsets.fromLTRB(24, 24, 24, 14),
title: Text('Confirm purchase'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Please select the guide language:'),
Flexible(
child: DropdownButtonFormField(
isExpanded: false,
isDense: true,
dropdownColor: Colors.white,
value: languageDropdownValue,
hint: Text(
'Preferred Language',
style: TextStyle(color: Colors.grey),
),
items: languages.map((map) {
return DropdownMenuItem(
value: map['code'],
child: Text(
map['value'],
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
onChanged: (String newValue) => dropDownFunction(newValue),
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
labelStyle: TextStyle(color: Colors.grey),
hintStyle: TextStyle(color: Colors.grey),
errorStyle: TextStyle(fontSize: 17.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
),
),
isError
? Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
'Please select a language',
style: TextStyle(
color: Colors.red,
),
),
),
)
: Container(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Text('Are you sure you want to purchase this audio guide?'),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
ElevatedButton(
onPressed: callbackFunction,
child: Text('Yes'),
),
SizedBox(
width: 40,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('No'),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.blue),
),
),
],
)
],
),
);
}
}
Solution (thanks to CbL) with a bit more functionality
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isLoading = false;
bool _downloaded = false;
File cardImage;
String _languageDropdownValue;
bool isError = false;
List<Map<String, String>> _languages = [
{'code': 'en', 'value': 'English'},
{'code': 'fr', 'value': 'French'},
];
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: _downloaded
? IconButton(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 0),
icon: Icon(
Icons.open_in_new,
size: 45.0,
color: Colors.green,
),
onPressed: () {
print('Open button pressed');
})
: _isLoading
? CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
)
: IconButton(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 0),
icon: Icon(
Icons.download_rounded,
size: 45.0,
color: Colors.green,
),
onPressed: () {
print('Download button pressed');
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setInnerState) {
return DownloadScreen(
callbackFunction: () =>
alertDialogCallback(setInnerState),
dropDownFunction: (value) =>
alertDialogDropdown(value, setInnerState),
isError: isError,
languages: _languages,
languageDropdownValue: _languageDropdownValue,
);
});
},
).then((value) => _languageDropdownValue = null);
}),
),
);
}
String alertDialogDropdown(String newValue, StateSetter setInnerState) {
setInnerState(() {
_languageDropdownValue = newValue;
isError = false;
});
return newValue;
}
alertDialogCallback(StateSetter setInnerState) {
if (_languageDropdownValue == null || _languageDropdownValue.isEmpty) {
setInnerState(() {
isError = true;
});
} else {
setInnerState(() {
isError = false;
startDownload();
});
}
}
void startDownload() async {
print('selected language is: $_languageDropdownValue');
Navigator.pop(context);
print('start download');
setState(() => _downloaded = true);
}
}
class DownloadScreen extends StatelessWidget {
DownloadScreen(
{@required this.callbackFunction,
@required this.dropDownFunction,
@required this.isError,
@required this.languages,
@required this.languageDropdownValue});
final Function callbackFunction;
final Function dropDownFunction;
final String languageDropdownValue;
final bool isError;
final List<Map<String, String>> languages;
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: EdgeInsets.fromLTRB(24, 24, 24, 14),
title: Text('Confirm purchase'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Please select the guide language:'),
Flexible(
child: DropdownButtonFormField(
isExpanded: false,
isDense: true,
dropdownColor: Colors.white,
value: languageDropdownValue,
hint: Text(
'Preferred Language',
style: TextStyle(color: Colors.grey),
),
items: languages.map((map) {
return DropdownMenuItem(
value: map['code'],
child: Text(
map['value'],
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
onChanged: (String newValue) => dropDownFunction(newValue),
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
labelStyle: TextStyle(color: Colors.grey),
hintStyle: TextStyle(color: Colors.grey),
errorStyle: TextStyle(fontSize: 17.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
),
),
isError
? Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
'Please select a language',
style: TextStyle(
color: Colors.red,
),
),
),
)
: Container(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Text('Are you sure you want to purchase this audio guide?'),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
ElevatedButton(
onPressed: callbackFunction,
child: Text('Yes'),
),
SizedBox(
width: 40,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('No'),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.blue),
),
),
],
)
],
),
);
}
}
Upvotes: 1
Views: 3861
Reputation: 532
Fixed your issue:
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io';
void main() {
runApp(MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
bool _isLoading = false;
bool _downloaded = false;
DownloadScreen downloadScreen;
File cardImage;
String _languageDropdownValue;
bool isError = false;
List<Map<String, String>> _languages = [
{'code': 'en', 'value': 'English'},
{'code': 'fr', 'value': 'French'},
];
@override
Widget build(BuildContext context) {
return Material(
child: Center(
child: _downloaded
? IconButton(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 0),
icon: Icon(
Icons.open_in_new,
size: 45.0,
color: Colors.green,
),
onPressed: () {
print('Open button pressed');
})
: _isLoading
? CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Colors.green),
)
: IconButton(
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 0),
icon: Icon(
Icons.download_rounded,
size: 45.0,
color: Colors.green,
),
onPressed: () {
print('Download button pressed');
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
return downloadScreen = DownloadScreen(
alertDialogCallback,
alertDialogDropdown,
isError,
_languages,
_languageDropdownValue,
);
});
},
);
}),
),
);
}
String alertDialogDropdown(String newValue) {
setState(() {
_languageDropdownValue = newValue;
});
return newValue;
}
alertDialogCallback() {
if (_languageDropdownValue == null || _languageDropdownValue.isEmpty) {
isError = true;
reloadDownloadScreen(true);
} else {
setState(() {
isError = false;
startDownload();
});
}
}
void startDownload() async {
print('selected language is: $_languageDropdownValue');
Navigator.pop(context);
print('start download');
setState(() => _downloaded = true);
}
void reloadDownloadScreen(bool isError) {
downloadScreen.refresh(isError);
}
}
class DownloadScreen extends StatefulWidget {
final Function alertDialogCallback;
final Function alertDialogDropdown;
final bool isError;
final List<Map<String, String>> languages;
_DownloadScreen _downloadScreen;
final String languageDropdownValue;
void refresh(bool isError){
_downloadScreen.refresh(isError);
}
DownloadScreen(this.alertDialogCallback, this.alertDialogDropdown, this.isError, this.languages, this.languageDropdownValue);
@override
_DownloadScreen createState(){
_downloadScreen = _DownloadScreen(
callbackFunction: alertDialogCallback,
dropDownFunction: alertDialogDropdown,
isError: isError,
languages: languages,
languageDropdownValue: languageDropdownValue
);
return _downloadScreen;
}
}
class _DownloadScreen extends State<DownloadScreen> {
_DownloadScreen(
{@required this.callbackFunction,
@required this.dropDownFunction,
@required this.isError,
@required this.languages,
@required this.languageDropdownValue
});
final Function callbackFunction;
final Function dropDownFunction;
final String languageDropdownValue;
bool isError;
final List<Map<String, String>> languages;
@override
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: EdgeInsets.fromLTRB(24, 24, 24, 14),
title: Text('Confirm purchase'),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Please select the guide language:'),
Flexible(
child: DropdownButtonFormField(
isExpanded: false,
isDense: true,
dropdownColor: Colors.white,
value: languageDropdownValue,
hint: Text(
'Preferred Language',
style: TextStyle(color: Colors.grey),
),
items: languages.map((map) {
return DropdownMenuItem(
value: map['code'],
child: Text(
map['value'],
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
onChanged: (String newValue) => dropDownFunction(newValue),
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
labelStyle: TextStyle(color: Colors.grey),
hintStyle: TextStyle(color: Colors.grey),
errorStyle: TextStyle(fontSize: 17.0),
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2),
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
),
),
),
isError
? Center(
child: Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
'Please select a language',
style: TextStyle(
color: Colors.red,
),
),
),
)
: Container(),
Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Text('Are you sure you want to purchase this audio guide?'),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
ElevatedButton(
onPressed: callbackFunction,
child: Text('Yes'),
),
SizedBox(
width: 40,
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text('No'),
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.blue),
),
),
],
)
],
),
);
}
void refresh(bool isError) {setState(() {
this.isError = isError;
});}
}
Main changes are as follows:
DownloadScreen
to extend StatefulWidget, creating in the process its corresponding _DownloadScreen
class which extends State<DownloadScreen>
alertDialogCallback()
function only refreshes the widgets from the _MyAppState
class, not the ones from _DownloadScreen
. In order to make this happen, created a private instance of DownloadScreen
in _MyAppState
. So when you enter alertDialogCallback()
and isError
is set to true
, you call DownloadScreen
, which will in turn call _DownloadScreen
which will the make a call to setState
refreshing the state of _DownloadScreen
instead of _MyAppState
.I don't like it, but works. If anyone with more flutter experience has a better workflow for this, feel free to comment or edit.
Upvotes: 0
Reputation: 824
From my understanding, the main problem is that you are calling setState, setting the _MyAppState's state which is not updating the dialog's internal state.
since you are using the StatefulBuilder, you need to pass the StateSetter to the value callback function.
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setInnerState) {
return DownloadScreen(
callbackFunction: () => alertDialogCallback(setInnerState),
dropDownFunction: (value) => alertDialogDropdown(value, setInnerState),
isError: isError,
languages: _languages,
languageDropdownValue: _languageDropdownValue,
);
});
},
);
And then set dialog's state with setInnerState, the dropdown will update when the dropdown selection is changed. I also updated the alertDialogCallback. It is the same reason that if you want to update dialog's state, you have to call setInnerState instead of the setState
String alertDialogDropdown(String newValue, StateSetter setInnerState) {
setInnerState(() { //use this because calling setState here is calling _MyAppState's state
_languageDropdownValue = newValue;
});
return newValue;
}
alertDialogCallback(StateSetter setInnerState) {
if (_languageDropdownValue == null || _languageDropdownValue.isEmpty) {
setInnerState(() {
isError = true;
});
} else {
setInnerState(() {
isError = false;
startDownload();
});
}
}
Upvotes: 2