Andres Silva
Andres Silva

Reputation: 892

Retrieve data from a Form within an Alert Dialog in Flutter

In order for my app's Alert Dialogs to be consistent, I have created a function that returns an Alert Dialog. The function does a bit of "painting", to make it look the way I want, but its basic structure is:

AlertDialog myAlertDialog({
  @required BuildContext context,
  @required Widget myContent,
  @required Function onAccept,}){

  return AlertDialog(
  content: Column(
      children = [
       myContent,
       FlatButton(child: Text("ok"), onPressed: onAccept),
       FlatButton(child: Text("cancel"), onPressed: Navigator.pop(context),
      ]
    )
  );
}

In this particular occasion, I am sending a Form to the myContent parameter. I am currently stuck on how to retrieve the data from the form in myContent and pass it to the function onAccept.

In case it might be helpful, the following is the basic structure of my form:

class myForm extends StatefulWidget {
  @override
  _myFormState createState() => _myFormState();
}

class _myFormState extends State<myForm> {
@override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: ListView(
        children: [
            TextFormField(onChanged: (val) {setState(() {var1 = val;});}, ... ), //Field 1
            TextFormField(onChanged: (val) {setState(() {var2 = val;});}, ... ), //Field 2
            //...
          ],
        ),
   );
 }
}

Thanks!

EDIT I noticed the following might also be useful/needed in order for my current problem to be more clear:

This is how I call the AlertDialog from the main widget.

class MainWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FlatButton(
        child: Text("launch Alert Dialog",),
        onPressed: () {showDialog(
                        context: context,
                        builder: (BuildContext context) {
                          return _launchAlertDialog(context);
                        },
                      );},
      ),
    );
  }
}

AlertDialog _launchAlertDialog(BuildContext context){
   return myAlertDialog(
     context: context,
     content: myForm(), //Data is captured here
     onAccept: (){}, //But I need to use it here
   );
}

Upvotes: 4

Views: 7104

Answers (3)

G&#225;bor
G&#225;bor

Reputation: 10224

Not radically different from your own solution but it leverages the internal saving mechanism of Form so it's less of a workaround. I have the following helper function to make it easier to use form dialogs from anywhere:

Future<bool> inputFormDialog(BuildContext context, {String? title, required Form Function(BuildContext, GlobalKey) formBuilder, String? okButton, String? cancelButton}) async {
  final formKey = GlobalKey<FormState>();
  final result = await showDialog<bool>(
    context: context,
    barrierDismissible: false,
    builder: (context) => AlertDialog(
      title: (title != null) ? Text(title) : null,
      content: formBuilder(context, formKey),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context, false),
          child: Text(cancelButton?.toUpperCase() ?? MaterialLocalizations.of(context).cancelButtonLabel),
        ),
        TextButton(
          onPressed: () {
            if (formKey.currentState!.validate()) {
              formKey.currentState!.save();
              Navigator.pop(context, true);
            }
          },
          child: Text(okButton?.toUpperCase() ?? MaterialLocalizations.of(context).okButtonLabel),
        ),
      ],
    ),
  );
  return result ?? false;
}

As you can see, it has some creature comforts as title, standard buttons with either standard or provided names. The small difference to your solution is that it handles the form state autonomically and calls the validation and saving functions when required. It can be used like this:

if (await inputFormDialog(context, formBuilder: (context, formKey) => buildFormDialog(context, formKey))) {
  setState(() {
    // handle the data set by the form innards, eg. formVariable
  });
}

where the form builder is a usual builder function:

Form buildFormDialog(BuildContext context, GlobalKey formKey) {
  return Form(
    key: formKey,
    child: Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        TextFormField(
          keyboardType: TextInputType.text,
          textInputAction: TextInputAction.done,
          decoration: ...,
          validator: ...,
          onSaved: (value) => formVariable = value!,
        ),
        // ...
      ],
    ),
  );
}

Upvotes: 0

Andres Silva
Andres Silva

Reputation: 892

I managed to solve it as follows:

(Note: it might not be the most elegant solution.. do let me know if there is a better way)

1) I created a Map that will hold the data.

2) I then created a function (called saveData) to add data to the Map. Then, I shared that function to the form myForm.

3) This allows me to easily handle the data at onAccept. These steps are below:

AlertDialog _launchAlertDialog(BuildContext context){
   Map data = {}; //This will hold my data.
   //And this function edits/adds data to the Map.
   void saveData(String variableName, dynamic value){data[variableName] = value;}
   return myAlertDialog(
     context: context,
     content: myForm(saveData: saveData), //Here, I pass the function to the form.
     onAccept: (){print(data['var1']);}, //Now, I can easily handle the data when the user presses accept.
   );
}

4) Finally, this is how the form myForm uses saveData to pass data to data:

class myForm extends StatefulWidget {
  final Function saveData;

  myForm({
    @required this.saveData,
  });

  @override
  _myFormState createState() => _myFormState();
}

class _myFormState extends State<myForm> {
@override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: ListView(
        children: [
            TextFormField(onChanged: (val) {setState(() {widget.saveData('var1', val);});}, ... ), //Field 1
            TextFormField(onChanged: (val) {setState(() {widget.saveData('var2', val);});}, ... ), //Field 2
            //...
          ],
        ),
   );
 }
}

Upvotes: 1

Kent
Kent

Reputation: 2547

If you read the method sig for Navigator.pop you will see

static void pop<T extends Object?>(BuildContext context, [ T? result ]) 

As you can see from the moths sig you can return two things in pop. context and the result data you want to send back to the previous screen. So something like this would work.

Navigator.pop(context, dataVaribleHere);

A more explicit definition is available if you are using VSCode and hover over the pop function a hint window will show.

The docs from the hint window:

void pop(BuildContext context, [T result]) package:flutter/src/widgets/navigator.dart

Pop the top-most route off the navigator that most tightly encloses the given context. The current route's [Route.didPop] method is called first. If that method returns false, then the route remains in the [Navigator]'s history (the route is expected to have popped some internal state; see e.g. [LocalHistoryRoute]). Otherwise, the rest of this description applies.

If non-null, result will be used as the result of the route that is popped; the future that had been returned from pushing the popped route will complete with result. Routes such as dialogs or popup menus typically use this mechanism to return the value selected by the user to the widget that created their route. The type of result, if provided, must match the type argument of the class of the popped route (T).

The popped route and the route below it are notified (see [Route.didPop], [Route.didComplete], and [Route.didPopNext]). If the [Navigator] has any [Navigator.observers], they will be notified as well (see [NavigatorObserver.didPop]).

The T type argument is the type of the return value of the popped route.

The type of result, if provided, must match the type argument of the class of the popped route (T).

You will need to catch the data in your screen that is pushing the alert dialog. Something like this. Notice that the containing function that it showing the dialog is an async function. This is important it lets us us the await keyword to wait for the dialog to dismiss before processing.

_showMyDialog() async {
var result = await showDialog(
    context: context,
    builder: (_) => new AlertDialog(
          title: new Text("Material Dialog"),
          content: new Text("Hey! I'm Coflutter!"),
          actions: <Widget>[
            FlatButton(
              child: Text('Close me!'),
              onPressed: () {
                Navigator.pop(context, dataVaribleHere);
              },
            )
          ],
        ));
          if (result != null) {
            setState(() {
              //Do stuff
            });
          }
  }

You will need to adapt this code to your specific situation but it shows the basic idea. Note that this code will not run as is. This will teach you how it's not for copy/pasting. dataVaribleHere is not a real variable it's telling you where to put your data.

Upvotes: 4

Related Questions