Add Form.onSaved (#30643)

When submitting data to a server, callers need a callback that will
get invoked after all the individual form fields are saved. If they
have a button that submits the form, they could just do this logic
in the click handler for the button (save the form, then submit to
the server), but if they have more ways than one to submit the form
(i.e. hitting enter while in a text form field), then it becomes
more convoluted and calls for a unified callback that will get
notified when the form is submitted.
diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart
index 4c3b120..e0ce2b8 100644
--- a/packages/flutter/lib/src/widgets/form.dart
+++ b/packages/flutter/lib/src/widgets/form.dart
@@ -72,6 +72,7 @@
     this.autovalidate = false,
     this.onWillPop,
     this.onChanged,
+    this.onSaved,
   }) : assert(child != null),
        super(key: key);
 
@@ -118,6 +119,13 @@
   /// will rebuild.
   final VoidCallback onChanged;
 
+  /// Called when the form is saved (after all the form fields have been saved).
+  ///
+  /// See also:
+  ///
+  ///  * [FormState.save]
+  final VoidCallback onSaved;
+
   @override
   FormState createState() => FormState();
 }
@@ -172,6 +180,8 @@
   void save() {
     for (FormFieldState<dynamic> field in _fields)
       field.save();
+    if (widget.onSaved != null)
+      widget.onSaved();
   }
 
   /// Resets every [FormField] that is a descendant of this [Form] back to its
diff --git a/packages/flutter/test/widgets/form_test.dart b/packages/flutter/test/widgets/form_test.dart
index 73cf030..25f4b16 100644
--- a/packages/flutter/test/widgets/form_test.dart
+++ b/packages/flutter/test/widgets/form_test.dart
@@ -6,9 +6,10 @@
 import 'package:flutter/material.dart';
 
 void main() {
-  testWidgets('onSaved callback is called', (WidgetTester tester) async {
+  testWidgets('onSaved callbacks are called', (WidgetTester tester) async {
     final GlobalKey<FormState> formKey = GlobalKey<FormState>();
     String fieldValue;
+    bool fieldModifiedSinceFormOnSaved;
 
     Widget builder() {
       return MediaQuery(
@@ -19,8 +20,12 @@
             child: Material(
               child: Form(
                 key: formKey,
+                onSaved: () { fieldModifiedSinceFormOnSaved = false; },
                 child: TextFormField(
-                  onSaved: (String value) { fieldValue = value; },
+                  onSaved: (String value) {
+                    fieldValue = value;
+                    fieldModifiedSinceFormOnSaved = true;
+                  },
                 ),
               ),
             ),
@@ -38,6 +43,7 @@
       formKey.currentState.save();
       // pump'ing is unnecessary because callback happens regardless of frames
       expect(fieldValue, equals(testValue));
+      expect(fieldModifiedSinceFormOnSaved, isFalse);
     }
 
     await checkText('Test');