Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why must I call didChange inside onChanged (in Flutter)?

Tags:

flutter

I am trying to use a DropdownButton within a Form to update a database record but unless I call didChange inside the onChanged callback the value written to the database is null instead of the value selected from the DropdownMenu.

From the documentation, the purpose of didChange is to trigger onChanged, so it doesn't seem to make any sense to have to call it from INSIDE onChanged. But if I don't, the code doesn't work.

See:

import 'package:flutter/material.dart';

void main() => runApp(MinimalReproducibleExample());

class MinimalReproducibleExample extends StatefulWidget {
  @override
  _MinimalReproducibleExampleState createState() => _MinimalReproducibleExampleState();
}

class _MinimalReproducibleExampleState extends State<MinimalReproducibleExample> {
  Enquiry _newEnquiry = Enquiry();
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  String _selectedAgent = '';
  String _selectedManager = '';
  @override
  Widget build(context) {
    return MaterialApp(
      home: Scaffold(
        body: Form(
          key: _formKey,
          child: Column(
            children: [
              FormField(
                builder: (state) {
                  return DropdownButton(
                    items: ['', 'A'].map((val) => DropdownMenuItem<String>(child: Text(val), value: val)).toList(),
                    value: _selectedAgent,
                    onChanged: (val) => setState(() {
                      _selectedAgent = val;
                      // state.didChange(val);
                    }),
                  );
                },
                onSaved: (val) => _newEnquiry.agent = val,
              ),
              FormField(
                builder: (state) {
                  return DropdownButton(
                    items: ['', '1'].map((val) => DropdownMenuItem<String>(child: Text(val), value: val)).toList(),
                    value: _selectedManager,
                    onChanged: (val) => setState(() {
                      _selectedManager = val;
                      state.didChange(val);
                    }),
                  );
                },
                onSaved: (val) => _newEnquiry.manager = val,
              ),
              RaisedButton(
                onPressed: () {
                  _formKey.currentState.save();
                  print(_newEnquiry.agent);
                  print(_newEnquiry.manager);
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Enquiry {
  String agent;
  String manager;
  Enquiry({this.agent, this.manager});
}

Running the code above (in Chrome, in case the device makes a difference) results in: "null 1" being printed. But when you un-comment the commented line the result is "A 1".

Why is it necessary? Or what am I doing wrong?

like image 288
Richard Avatar asked Sep 06 '25 02:09

Richard


1 Answers

You can copy paste run full code below
Reason is FormField builder has it's own State, outside onSaved can only get value after you call state.didChange
For detail, you can reference Building the CounterFormField part of https://medium.com/saugo360/creating-custom-form-fields-in-flutter-85a8f46c2f41

From document : the most important of them is the didChange method, which we should call with the new value whenever the value of the field changes. This method updates the field’s value, informs the parent form of the change, and rebuilds the widget.

In your case, you can use DropdownButtonFormField instead

code snippet

DropdownButtonFormField<int>(
                value: _ratingController,
                items: [1, 2, 3, 4, 5]
                    .map((label) => DropdownMenuItem(
                          child: Text(label.toString()),
                          value: label,
                        ))
                    .toList(),
                hint: Text('Rating'),
                onChanged: (value) {
                  setState(() {
                    _ratingController = value;
                  });
                },
                onSaved: (val) {
                  print('onSaved drowndownformfield $val');
                },
              ),

working demo

enter image description here

output

I/flutter (26553): validating  null
I/flutter (26553): onSaved A null
I/flutter (26553): onSaved 1 1
I/flutter (26553): onSaved drowndownformfield 2
I/flutter (26553): null
I/flutter (26553): 1

full code

import 'package:flutter/material.dart';

class MinimalReproducibleExample extends StatefulWidget {
  @override
  _MinimalReproducibleExampleState createState() =>
      _MinimalReproducibleExampleState();
}

class _MinimalReproducibleExampleState
    extends State<MinimalReproducibleExample> {
  Enquiry _newEnquiry = Enquiry();
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  String _selectedAgent = '';
  String _selectedManager = '';
  int _ratingController;

  @override
  Widget build(context) {
    return MaterialApp(
      home: Scaffold(
        body: Form(
          key: _formKey,
          child: Column(
            children: [
              FormField(
                builder: (state) {
                  return DropdownButton(
                      items: ['', 'A']
                          .map((val) => DropdownMenuItem<String>(
                              child: Text(val), value: val))
                          .toList(),
                      value: _selectedAgent,
                      onChanged: (val) => setState(() {
                            _selectedAgent = val;
                            // state.didChange(val);
                          }));
                },
                validator: (val) {
                  print('validating  $val');
                  return null;
                },
                onSaved: (val) {
                  print('onSaved A $val');
                  _newEnquiry.agent = val;
                },
              ),
              FormField(
                builder: (state) {
                  return DropdownButton(
                    items: ['', '1']
                        .map((val) => DropdownMenuItem<String>(
                            child: Text(val), value: val))
                        .toList(),
                    value: _selectedManager,
                    onChanged: (val) => setState(() {
                      _selectedManager = val;
                      state.didChange(val);
                    }),
                  );
                },
                onSaved: (val) {
                  print('onSaved 1 $val');
                  _newEnquiry.manager = val;
                },
              ),
              DropdownButtonFormField<int>(
                value: _ratingController,
                items: [1, 2, 3, 4, 5]
                    .map((label) => DropdownMenuItem(
                          child: Text(label.toString()),
                          value: label,
                        ))
                    .toList(),
                hint: Text('Rating'),
                onChanged: (value) {
                  setState(() {
                    _ratingController = value;
                  });
                },
                onSaved: (val) {
                  print('onSaved drowndownformfield $val');
                },
              ),
              RaisedButton(
                onPressed: () {
                  if (_formKey.currentState.validate()) {
                    _formKey.currentState.save();
                    print(_newEnquiry.agent);
                    print(_newEnquiry.manager);
                  }
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class Enquiry {
  String agent;
  String manager;
  Enquiry({this.agent, this.manager});
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(
        title: "test",
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

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

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Expanded(child: MinimalReproducibleExample()),
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
like image 129
chunhunghan Avatar answered Sep 08 '25 00:09

chunhunghan