Skip to content

Commit b91eca4

Browse files
authored
Merge pull request #41 from flutter/form
Add form skill
2 parents fbc47fd + 87f211f commit b91eca4

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

resources/flutter_skills.yaml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,56 @@
347347
- https://docs.flutter.dev/cookbook/design/themes
348348
- https://docs.flutter.dev/ui/adaptive-responsive/idioms
349349
- https://docs.flutter.dev/ui
350+
- name: flutter-form
351+
description: Build a form with validation
352+
instructions: |
353+
To build a form with validation, you need to create a `Form` widget with a
354+
`GlobalKey`. This `GlobalKey` uniquely identifies the `Form` widget and
355+
allows for validation later. It's recommended to create the form as a
356+
`StatefulWidget` to store the `GlobalKey<FormState>()` once, as generating a
357+
new `GlobalKey` each time the `build` method runs is resource-expensive. The
358+
`Form` widget acts as a container for grouping and validating multiple form
359+
fields. While a `GlobalKey` is the recommended way to access a form,
360+
`Form.of()` can be used in more complex widget trees.
361+
362+
Next, add a `TextFormField` with validation logic. The `TextFormField`
363+
widget renders a material design text field and can display validation
364+
errors. Input validation is done by providing a `validator()` function to
365+
the `TextFormField`. If the user's input is invalid, the `validator`
366+
function should return a `String` containing an error message. If there are
367+
no errors, the `validator` must return `null`. For example, a validator can
368+
ensure the `TextFormField` isn't empty by returning a message like 'Please
369+
enter some text' if the value is null or empty, otherwise returning null.
370+
`TextFormField` is a convenience widget that pre-wraps a `TextField` in a
371+
`FormField`. Each individual form field should be wrapped in a `FormField`
372+
widget with the `Form` widget as a common ancestor.
373+
374+
Finally, create a button to validate and submit the form. When the user
375+
attempts to submit the form, you check if the form is valid. To validate the
376+
form, use the `_formKey` created earlier. The `_formKey.currentState`
377+
accessor provides access to the `FormState`, which is automatically created
378+
by Flutter when building a `Form`. The `FormState` class contains the
379+
`validate()` method. When `validate()` is called, it runs the `validator()`
380+
function for each text field in the form. If all validations pass,
381+
`validate()` returns `true`. If any text field contains errors, `validate()`
382+
rebuilds the form to display error messages and returns `false`. If the form
383+
is valid, you can display a success message (e.g., a `SnackBar`). If it's
384+
not valid, the error messages from the `TextFormField` validators will be
385+
displayed. The `FormState` also allows you to save, reset, and validate each
386+
`FormField` that descends from the `Form`.
387+
resources:
388+
- https://docs.flutter.dev/ai-best-practices/developer-experience
389+
- https://docs.flutter.dev/app-architecture/recommendations
390+
- https://docs.flutter.dev/cookbook/forms
391+
- https://docs.flutter.dev/cookbook/forms/text-input
392+
- https://docs.flutter.dev/cookbook/forms/validation
393+
- https://docs.flutter.dev/flutter-for/react-native-devs
394+
- https://docs.flutter.dev/flutter-for/xamarin-forms-devs
395+
- https://docs.flutter.dev/get-started/fundamentals/user-input
396+
- https://docs.flutter.dev/release/breaking-changes/form-field-autovalidation-api
397+
- https://docs.flutter.dev/ui/adaptive-responsive/best-practices
398+
- https://docs.flutter.dev/ui/interactivity/input
399+
350400
- name: flutter-routing-and-navigation
351401
description: Move between or deep link to different screens or routes within a Flutter application
352402
instructions: |

skills/flutter-form/SKILL.md

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
---
2+
name: "flutter-form"
3+
description: "Build a form with validation"
4+
metadata:
5+
model: "models/gemini-3.1-pro-preview"
6+
last_modified: "Wed, 11 Mar 2026 16:47:50 GMT"
7+
8+
---
9+
# Flutter Form Validation
10+
11+
## Goal
12+
Implements stateful form validation in Flutter using `Form`, `TextFormField`, and `GlobalKey<FormState>`. Manages validation state efficiently without unnecessary key regeneration and handles user input validation workflows. Assumes a pre-existing Flutter environment with Material Design dependencies available.
13+
14+
## Decision Logic
15+
When implementing form validation, follow this decision tree to determine the flow of state and UI updates:
16+
1. **User triggers submit action:**
17+
- Call `_formKey.currentState!.validate()`.
18+
2. **Does `validate()` return `true`?**
19+
- **Yes (Valid):** Proceed with data processing (e.g., API call, local storage). Trigger success UI feedback (e.g., `SnackBar`, navigation).
20+
- **No (Invalid):** The `FormState` automatically rebuilds the `TextFormField` widgets to display the `String` error messages returned by their respective `validator` functions. Halt submission.
21+
22+
## Instructions
23+
24+
1. **Initialize the Stateful Form Container**
25+
Create a `StatefulWidget` to hold the form. Instantiate a `GlobalKey<FormState>` exactly once within the `State` class to prevent resource-expensive key regeneration during `build` cycles.
26+
```dart
27+
import 'package:flutter/material.dart';
28+
29+
class CustomValidatedForm extends StatefulWidget {
30+
const CustomValidatedForm({super.key});
31+
32+
@override
33+
State<CustomValidatedForm> createState() => _CustomValidatedFormState();
34+
}
35+
36+
class _CustomValidatedFormState extends State<CustomValidatedForm> {
37+
// Instantiate the GlobalKey once in the State object
38+
final _formKey = GlobalKey<FormState>();
39+
40+
@override
41+
Widget build(BuildContext context) {
42+
return Form(
43+
key: _formKey,
44+
child: Column(
45+
crossAxisAlignment: CrossAxisAlignment.start,
46+
children: <Widget>[
47+
// Form fields will be injected here
48+
],
49+
),
50+
);
51+
}
52+
}
53+
```
54+
55+
2. **Implement TextFormFields with Validation Logic**
56+
Inject `TextFormField` widgets into the `Form`'s widget tree. Provide a `validator` function for each field.
57+
```dart
58+
TextFormField(
59+
decoration: const InputDecoration(
60+
hintText: 'Enter your email',
61+
labelText: 'Email',
62+
),
63+
validator: (String? value) {
64+
if (value == null || value.isEmpty) {
65+
return 'Please enter an email address';
66+
}
67+
if (!value.contains('@')) {
68+
return 'Please enter a valid email address';
69+
}
70+
// Return null if the input is valid
71+
return null;
72+
},
73+
onSaved: (String? value) {
74+
// Handle save logic here
75+
},
76+
)
77+
```
78+
79+
3. **Implement the Submit Action and Validation Trigger**
80+
Create a button that accesses the `FormState` via the `GlobalKey` to trigger validation.
81+
```dart
82+
Padding(
83+
padding: const EdgeInsets.symmetric(vertical: 16.0),
84+
child: ElevatedButton(
85+
onPressed: () {
86+
// Validate returns true if the form is valid, or false otherwise.
87+
if (_formKey.currentState!.validate()) {
88+
// Save the form fields if necessary
89+
_formKey.currentState!.save();
90+
91+
// Provide success feedback
92+
ScaffoldMessenger.of(context).showSnackBar(
93+
const SnackBar(content: Text('Processing Data')),
94+
);
95+
}
96+
},
97+
child: const Text('Submit'),
98+
),
99+
)
100+
```
101+
102+
4. **STOP AND ASK THE USER:**
103+
Pause implementation and ask the user for the following context:
104+
* "What specific fields do you need in this form?"
105+
* "What are the exact validation rules for each field (e.g., regex patterns, minimum lengths)?"
106+
* "What action should occur upon successful validation (e.g., API payload submission, navigation)?"
107+
108+
5. **Validate-and-Fix Loop**
109+
After generating the form, verify the following:
110+
* Ensure `_formKey.currentState!.validate()` is null-checked properly using the bang operator (`!`) or safe calls if the key might be detached.
111+
* Verify that every `validator` function explicitly returns `null` on success. Returning an empty string (`""`) will trigger an error state with no text.
112+
113+
## Constraints
114+
* **DO NOT** instantiate the `GlobalKey<FormState>` inside the `build` method. It must be a persistent member of the `State` class.
115+
* **DO NOT** use a `StatelessWidget` for the form container unless the `GlobalKey` is being passed down from a stateful parent.
116+
* **DO NOT** use standard `TextField` widgets if you require built-in form validation; you must use `TextFormField` (which wraps `TextField` in a `FormField`).
117+
* **ALWAYS** return `null` from a `validator` function when the input is valid.
118+
* **ALWAYS** ensure the `Form` widget is a common ancestor to all `TextFormField` widgets that need to be validated together.

0 commit comments

Comments
 (0)