Buttons, text fields and gestures

Flutter · Essentials · 12 min

Interactive apps need input, and controllers must be managed correctly to avoid leaks and lost text.

Buttons: FilledButton, ElevatedButton, OutlinedButton, TextButton and IconButton take an onPressed callback. Pass null to show the button as disabled.

Gestures on arbitrary widgets: wrap them in GestureDetector (onTap, onLongPress, onPanUpdate...) or InkWell for a Material ripple.

Text input uses TextField (or TextFormField inside a form):

  • onChanged: (text) => ... fires on every keystroke.
  • A `TextEditingController` reads and sets the text programmatically: controller.text, controller.clear(). Create it in initState (or as a field) of a State and dispose it in dispose.
  • decoration: InputDecoration(labelText: ..., hintText: ..., errorText: ...) styles it.
  • keyboardType, textInputAction, obscureText for passwords, autofocus, and onSubmitted.
  • A FocusNode controls focus; FocusScope.of(context).unfocus() hides the keyboard.

Selection controls are controlled widgets: Switch(value: on, onChanged: (v) => setState(() => on = v)), and likewise Checkbox, Radio, Slider, DropdownMenu. The widget shows whatever value you pass; it only reports changes, so you must store the new value and rebuild.

When the keyboard opens, Scaffold resizes its body by default; put forms in a scroll view so fields aren't hidden.

Example

import 'package:flutter/material.dart';
class NewTodo extends StatefulWidget {
  const NewTodo({super.key});
  @override
  State<NewTodo> createState() => _NewTodoState();
}

class _NewTodoState extends State<NewTodo> {
  final _controller = TextEditingController();
  bool _urgent = false;

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

  @override
  Widget build(BuildContext context) => Column(children: [
        TextField(controller: _controller, decoration: const InputDecoration(labelText: 'Task')),
        SwitchListTile(title: const Text('Urgent'), value: _urgent,
            onChanged: (v) => setState(() => _urgent = v)),
        FilledButton(
          onPressed: () { debugPrint('${_controller.text} urgent=$_urgent'); _controller.clear(); },
          child: const Text('Add'),
        ),
      ]);
}

Remember

  • onPressed: null disables a button
  • Controllers are created in State and disposed in dispose
  • Switches, checkboxes and sliders are controlled: store the value and rebuild

Watch out for

  • Creating a TextEditingController inside build, which resets the text on every rebuild and leaks.

Check yourself

  1. A Switch's onChanged handler is `(v) {}` — what happens when the user taps it?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.flutter.dev/cookbook/forms/text-input