1

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),
                ),
              ),
            ],
          )
        ],
      ),
    );
  }
}
RedHappyLlama
  • 53
  • 1
  • 8
  • Can you elaborate on the question? What is your expected output vs what is the current output. – afarre Apr 30 '21 at 08:53
  • Hi, thanks, I've updated the question which hopefully explains. The code provided can be used to recreate the issue. Let me know if anymore info is required! Thanks! – RedHappyLlama Apr 30 '21 at 09:08
  • In a nutshell, your issue is that DownloadScreen extends StatelessWidget. StatelessWidget means [the widgets never change](https://flutter.dev/docs/development/ui/interactive#stateful-and-stateless-widgets), that's why when calling SetState, the DownloadScreen class does not refresh its contents causing the error never showing up. You must make the class extend StatefulWidget, and SetState will work. I'm bad at flutter too, and I'm trying to change the class to StatefulWidget, but I'm failing. Try yourself see if you manage. – afarre Apr 30 '21 at 09:48

2 Answers2

2

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();
      });
    }
}
CbL
  • 734
  • 5
  • 22
  • Hi, many thanks for the response. It doesn't seem to have made a difference unfortunately and my experience / knowledge of Flutter isn't good enough to understand why. – RedHappyLlama Apr 30 '21 at 08:44
  • edited the post, same as the alertDialogDropdown, if you want the dialog's state update, you need to change that dialog's call back to use setInnerState – CbL Apr 30 '21 at 09:35
  • CbL, take a look at my comment under the original post. I believe the issue is that DownloadScreen extends StatelessWidget, and not StatefulWidget, thus the calls to SetState are ignored. I tried changing the code but I'm not good at flutter. Can you confirm that is the issue? Can you make the changes needed? – afarre Apr 30 '21 at 09:49
  • 1
    @afarre I dunt think that is the main issue. Both **alertDialogCallback** and **alertDialogDropdown** method are placed at inside the _MyAppState class, not inside the StatefulBuilder. Thus, the setState is in fact setting _MyAppState which, as result, will re-call the **build** function of _MyAppState. **showDialog** is a special situation that it is always on top of the widget. you can imagine it is separated from the _MyAppState build UI rebuild. So even if you change it to StatefulWidget with those call back handle out side the DocumentScreen, the issue will still exist. – CbL Apr 30 '21 at 10:53
  • That does the trick! Perfect, thank you very much CbL!! – RedHappyLlama Apr 30 '21 at 10:54
  • Unless you put those method inside after changed to StatefulWidget. Then the setState at that time is setting DocumentScreen's state. (sorry too long cannot type all :-) ) – CbL Apr 30 '21 at 10:54
  • I just posted an answer. Basically called a function inside `_DownloadScreen` all the way from `_MyAppState`. I've run it and works, but I think is a dirty solution. Is there any better way to implement this? – afarre Apr 30 '21 at 10:56
  • 1
    @afarre you are exactly demonstrating my comment about putting the setState callback logic inside after DocumentScreen changed to StatefulWidget. Both of us are correct. It just depends on how you want to separate the logic. For my own practice, i usually using StatefulBuilder way when the dialog is using more than one place, having slightly different reaction only after callback and logic is simple. And I will separate a StatefulWidget when the dialog is after unique and complex logic and hardly re-usable. After all, just depends on the situation. – CbL Apr 30 '21 at 11:03
  • @RedHappyLlama im glad this help you! – CbL Apr 30 '21 at 11:05
0

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.

afarre
  • 512
  • 6
  • 18