5 / 11
Guide Book · Chapter 5

Input, gestures & forms

Buttons and gesture detectors, text input and controllers, and the `Form` machinery for validation.

Buttons and taps

dart
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: Buttons())));

class Buttons extends StatefulWidget {
  const Buttons({super.key});

  @override
  State<Buttons> createState() => _ButtonsState();
}

class _ButtonsState extends State<Buttons> {
  String _last = 'nothing yet';

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('last action: $_last'),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () => setState(() => _last = 'elevated'),
            child: const Text('Elevated'),
          ),
          FilledButton(
            onPressed: () => setState(() => _last = 'filled'),
            child: const Text('Filled'),
          ),
          OutlinedButton(
            onPressed: () => setState(() => _last = 'outlined'),
            child: const Text('Outlined'),
          ),
          TextButton(
            onPressed: () => setState(() => _last = 'text'),
            child: const Text('Text'),
          ),
          IconButton(
            onPressed: () => setState(() => _last = 'icon'),
            icon: const Icon(Icons.favorite),
          ),
          const ElevatedButton(
            onPressed: null,             // null disables the button
            child: Text('Disabled'),
          ),
        ],
      ),
    );
  }
}

onPressed: null is how a button is disabled — there is no enabled property.

Gestures

dart
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: Gestures())));

class Gestures extends StatefulWidget {
  const Gestures({super.key});

  @override
  State<Gestures> createState() => _GesturesState();
}

class _GesturesState extends State<Gestures> {
  String _last = 'try tapping, long-pressing or dragging';
  Offset _position = Offset.zero;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(_last),
          Text('drag offset: ${_position.dx.toStringAsFixed(0)},'
              ' ${_position.dy.toStringAsFixed(0)}'),
          const SizedBox(height: 16),
          GestureDetector(
            onTap: () => setState(() => _last = 'tap'),
            onDoubleTap: () => setState(() => _last = 'double tap'),
            onLongPress: () => setState(() => _last = 'long press'),
            onPanUpdate: (details) => setState(() {
              _last = 'pan';
              _position += details.delta;
            }),
            child: Container(
              width: 160,
              height: 160,
              color: Colors.indigo.shade100,
              child: const Center(child: Text('gesture area')),
            ),
          ),
          const SizedBox(height: 16),
          // InkWell adds the Material ripple that GestureDetector lacks.
          InkWell(
            onTap: () => setState(() => _last = 'inkwell tap'),
            child: const Padding(
              padding: EdgeInsets.all(16),
              child: Text('InkWell — with a ripple'),
            ),
          ),
        ],
      ),
    );
  }
}

💡 Tip: Prefer InkWell or a button over a bare GestureDetector for anything the user taps. You get the ripple, the correct hit area, keyboard focus and screen-reader semantics for free — all of which a GestureDetector leaves you to build.

Text input

dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: TextInput())));

class TextInput extends StatefulWidget {
  const TextInput({super.key});

  @override
  State<TextInput> createState() => _TextInputState();
}

class _TextInputState extends State<TextInput> {
  final _controller = TextEditingController();
  String _submitted = '';

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          TextField(
            controller: _controller,
            decoration: const InputDecoration(
              labelText: 'Name',
              hintText: 'e.g. Ada Lovelace',
              border: OutlineInputBorder(),
              prefixIcon: Icon(Icons.person),
            ),
            textInputAction: TextInputAction.next,
            onSubmitted: (value) => setState(() => _submitted = value),
          ),
          const SizedBox(height: 12),
          TextField(
            keyboardType: TextInputType.number,
            inputFormatters: [FilteringTextInputFormatter.digitsOnly],
            decoration: const InputDecoration(labelText: 'Digits only'),
          ),
          const SizedBox(height: 12),
          const TextField(
            obscureText: true,
            decoration: InputDecoration(labelText: 'Password'),
          ),
          const SizedBox(height: 12),
          const TextField(
            maxLines: 3,
            decoration: InputDecoration(labelText: 'Notes', alignLabelWithHint: true),
          ),
          const SizedBox(height: 12),
          Text('submitted: $_submitted'),
        ],
      ),
    );
  }
}

Forms and validation

dart
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: SignupForm())));

class SignupForm extends StatefulWidget {
  const SignupForm({super.key});

  @override
  State<SignupForm> createState() => _SignupFormState();
}

class _SignupFormState extends State<SignupForm> {
  final _formKey = GlobalKey<FormState>();
  final _email = TextEditingController();
  final _password = TextEditingController();
  String _result = '';

  @override
  void dispose() {
    _email.dispose();
    _password.dispose();
    super.dispose();
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      setState(() => _result = 'valid: ${_email.text}');
    } else {
      setState(() => _result = 'please fix the errors above');
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Form(
        key: _formKey,
        autovalidateMode: AutovalidateMode.onUserInteraction,
        child: Column(
          children: [
            TextFormField(
              controller: _email,
              decoration: const InputDecoration(labelText: 'Email'),
              keyboardType: TextInputType.emailAddress,
              validator: (value) {
                if (value == null || value.isEmpty) return 'Email is required';
                if (!value.contains('@')) return 'That does not look like an email';
                return null;                      // null means valid
              },
            ),
            TextFormField(
              controller: _password,
              decoration: const InputDecoration(labelText: 'Password'),
              obscureText: true,
              validator: (value) =>
                  (value == null || value.length < 8) ? 'At least 8 characters' : null,
            ),
            const SizedBox(height: 16),
            FilledButton(onPressed: _submit, child: const Text('Sign up')),
            const SizedBox(height: 8),
            Text(_result),
          ],
        ),
      ),
    );
  }
}

Selection widgets

dart
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: Scaffold(body: Selections())));

class Selections extends StatefulWidget {
  const Selections({super.key});

  @override
  State<Selections> createState() => _SelectionsState();
}

class _SelectionsState extends State<Selections> {
  bool _checked = false;
  bool _switched = true;
  String _radio = 'a';
  double _slider = 30;
  String _dropdown = 'one';

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        CheckboxListTile(
          title: const Text('Checkbox'),
          value: _checked,
          onChanged: (v) => setState(() => _checked = v ?? false),
        ),
        SwitchListTile(
          title: const Text('Switch'),
          value: _switched,
          onChanged: (v) => setState(() => _switched = v),
        ),
        for (final option in ['a', 'b'])
          RadioListTile<String>(
            title: Text('Option $option'),
            value: option,
            groupValue: _radio,
            onChanged: (v) => setState(() => _radio = v ?? 'a'),
          ),
        Slider(
          value: _slider,
          max: 100,
          divisions: 10,
          label: _slider.round().toString(),
          onChanged: (v) => setState(() => _slider = v),
        ),
        DropdownButton<String>(
          value: _dropdown,
          isExpanded: true,
          items: const [
            DropdownMenuItem(value: 'one', child: Text('One')),
            DropdownMenuItem(value: 'two', child: Text('Two')),
          ],
          onChanged: (v) => setState(() => _dropdown = v ?? 'one'),
        ),
      ],
    );
  }
}

Every one follows the same controlled pattern: the widget takes a value and reports changes through onChanged. It holds nothing itself, which is why setState is always required.

Reference: Forms and Handling gestures on docs.flutter.dev.

main.dart
Loading editor…
Flutter runs on your computer
The website can't run Flutter. Write your code here and compare it with the solution (the lightbulb).

Search

Jump to a lesson or a past run