Search code examples
flutterdartbuilderflutter-alertdialog

Flutter builder for AlertDialog not being updated by SetState in parent


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

Solution

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