Flutter builder for AlertDialog not being updated by SetState in parent

3.6k Views Asked by At

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),
                ),
              ),
            ],
          )
        ],
      ),
    );
  }
}
2

There are 2 best solutions below

9
On BEST ANSWER

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();
      });
    }
}
0
On

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:

  • Changed DownloadScreen to extend StatefulWidget, creating in the process its corresponding _DownloadScreen class which extends State<DownloadScreen>
  • The setState you used in the 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.