Your First Flutter Forms: Creating and Validating User Input

Maxim Gorin
6 min readSep 5, 2024

--

When you’re building an app, capturing and validating user input is where the magic happens — it’s how your app starts to truly engage with users. This article is part of our series aimed at helping you, as a beginner, get hands-on with Flutter and Dart. In our last session, Make Your Flutter Apps Come Alive: Simple Projects for Beginners, we explored how to make your app interactive. Now, we’re going to dive into one of the most crucial aspects of app development: working with text input fields and creating forms.

Flutter — Build apps for any screen

Why Text Input Matters

Text input is the gateway between your users and your app. It’s where users tell your app what they want, whether it’s entering a name, writing a message, or searching for something specific. But it’s not just about collecting text — it’s about ensuring that the text is valid and useful for your app’s needs.

Creating a Simple Text Field That Responds

Simple Text Field

Let’s start with a straightforward example — a text field that doesn’t just collect data but also responds instantly to what the user types. Instead of passively waiting for a submission, your app can provide immediate feedback, making the interaction more engaging.

import 'package:flutter/material.dart';

class SimpleTextField extends StatefulWidget {
@override
_SimpleTextFieldState createState() => _SimpleTextFieldState();
}

class _SimpleTextFieldState extends State<SimpleTextField> {
final TextEditingController _controller = TextEditingController();
String _displayName = '';

void _showName() {
setState(() {
_displayName = _controller.text;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("What's your name?")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextField(
controller: _controller,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Enter your name',
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _showName,
child: Text('Submit'),
),
SizedBox(height: 20),
Text(
_displayName.isEmpty ? 'Please enter your name' : 'Nice to meet you, $_displayName!',
style: TextStyle(fontSize: 24),
),
],
),
),
);
}
}

void main() => runApp(MaterialApp(home: SimpleTextField()));

Why This Example Works

This isn’t just a simple text field — it’s a tool that transforms static input into a dynamic interaction. By immediately reflecting what the user types, your app becomes more engaging and feels responsive to their actions.

Explanation of New Concepts

  • TextEditingController: This is a class that provides a way to control the text being entered into a TextField. It allows you to retrieve the current value of the text field or clear it if needed. In this example, we use it to get the user's input and display it back to them.
  • TextField: The TextField widget is where users enter text. It’s a core component for collecting text input. The decoration property is used to customize the look and feel of the text field, including adding a label or placeholder text.
  • setState(): Although introduced in earlier examples, it’s crucial here because it tells Flutter that something has changed in the widget’s state (in this case, the text entered by the user), so the UI needs to be rebuilt with the new data.

Building a Multi-Field Form with Validation

Multi-Field Form with Validation

Let’s go a step further and create a full-fledged form. This form will capture a user’s name, email, and a short bio, but it won’t just stop there — it will also validate the input to ensure that the data you’re collecting is useful and correct.

import 'package:flutter/material.dart';

class UserForm extends StatefulWidget {
@override
_UserFormState createState() => _UserFormState();
}

class _UserFormState extends State<UserForm> {
final _formKey = GlobalKey<FormState>();
String _name = '';
String _email = '';
String _bio = '';

void _submitForm() {
if (_formKey.currentState?.validate() ?? false) {
_formKey.currentState?.save();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Form Submitted Successfully')));

print("name: $_name");
print("_email: $_email");
print("bio: $_bio");
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Tell us about yourself')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'Name'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Your name is required';
}
return null;
},
onSaved: (value) {
_name = value ?? '';
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a valid email address';
}
if (!RegExp(r'^[^@]+@[^@]+\\.[^@]+').hasMatch(value)) {
return "That doesn't look like an email address";
}
return null;
},
onSaved: (value) {
_email = value ?? '';
},
),
TextFormField(
decoration: InputDecoration(labelText: 'Bio'),
maxLines: 3,
onSaved: (value) {
_bio = value ?? '';
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm,
child: Text('Submit'),
),
],
),
),
),
);
}
}

void main() => runApp(MaterialApp(home: UserForm()));

Explanation of New Concepts

  • GlobalKey<FormState>: This key uniquely identifies the form and allows you to perform actions like validating all the fields or saving their values at once. In Flutter, forms are a collection of multiple TextFormField widgets, and this key helps manage them as a single entity.
  • Form Widget: The Form widget groups together multiple TextFormField widgets and provides methods to manage their state. This widget is essential for creating and managing complex forms.
  • TextFormField: Similar to TextField, but with built-in support for validation. This widget is more powerful when working with forms as it easily integrates with form validation and submission.
  • validator: The validator function is used to check if the input meets certain criteria. If the input is valid, it returns null; if not, it returns an error message that will be displayed to the user.
  • onSaved: This function is called when the form is submitted and saves the current value of the field to a variable, making it easier to handle the form data later.

Let’s Build Something Real: A Note-Taking App

A Note-Taking App

To bring everything together, we’ll create a note-taking app that allows users to jot down ideas, thoughts, or reminders. This app will be simple yet practical, demonstrating how you can capture and display data in real-time.

import 'package:flutter/material.dart';

class NoteTakingApp extends StatefulWidget {
@override
_NoteTakingAppState createState() => _NoteTakingAppState();
}

class _NoteTakingAppState extends State<NoteTakingApp> {
final _titleController = TextEditingController();
final _contentController = TextEditingController();
final List<Map<String, String>> _notes = [];

void _addNote() {
setState(() {
_notes.add({
'title': _titleController.text,
'content': _contentController.text,
});
_titleController.clear();
_contentController.clear();
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Your Notes')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
controller: _titleController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Title',
),
),
SizedBox(height: 10),
TextField(
controller: _contentController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Content',
),
maxLines: 3,
),
SizedBox(height: 10),
ElevatedButton(
onPressed: _addNote,
child: Text('Add Note'),
),
Expanded(
child: ListView.builder(
itemCount: _notes.length,
itemBuilder: (context, index) {
final note = _notes[index];
return ListTile(
title: Text(note['title'] ?? ''),
subtitle: Text(note['content'] ?? ''),
);
},
),
),
],
),
),
);
}
}

void main() => runApp(MaterialApp(home: NoteTakingApp()));

Explanation of New Concepts

  • List<Map<String, String>>: This list stores each note as a map, where the title and content are stored as key-value pairs. This structure is simple yet effective for managing a list of notes.
  • ListView.builder: This widget efficiently creates a scrollable list of items, only building the widgets that are currently visible on the screen. This is essential for performance when working with long lists.
  • TextEditingController: Used again here to capture the text entered by the user in both the title and content fields. After adding the note to the list, the controllers are cleared, resetting the input fields for the next entry.
  • Expanded: This widget makes the list of notes take up as much space as possible, ensuring that the note list remains scrollable while other elements stay in place.

Conclusion

Building apps that handle text input effectively is about creating interactions that feel natural and responsive. Whether it’s a simple name field that greets the user or a full-featured note-taking app, these examples show that it’s the little details — like validation and immediate feedback — that make the difference between a functional app and one that users love to engage with. Keep pushing the boundaries of what you can do with text input, and you’ll be on your way to creating truly impactful applications.

--

--

Maxim Gorin
Maxim Gorin

Written by Maxim Gorin

Team lead in mobile development with a passion for Fintech and Flutter. Sharing insights and stories from the tech and dev world on this blog.

No responses yet