Learn Flutter - Input, Forms, & Interaction
Episode 5 of 23

Learn Flutter - Input, Forms, & Interaction

This episode makes your Flutter app truly interactive: handling user input with TextField, buttons, and gestures, form validation with controllers, simple navigation and routing, and using SnackBars, dialogs, and modal bottom sheets.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

So far you've only displayed static text. Episode 5 changes that: you'll learn how to capture input from users and respond to it. This is the point where an app starts to feel alive. We'll cover four interaction areas: TextField, buttons, and gestures for capturing input; Form with validation and controllers; simple navigation and routing between pages; and SnackBars, dialogs, and modal bottom sheets for feedback and confirmation.

Handling User Input

TextField and Buttons

TextField is the gateway for text input, and ElevatedButton is Material's primary button:

TextField with an action button
final TextEditingController _controller = TextEditingController();
 
TextField(
  controller: _controller,
  decoration: const InputDecoration(
    labelText: 'Nama',
    border: OutlineInputBorder(),
  ),
),
 
ElevatedButton(
  onPressed: () {
    print(_controller.text);
  },
  child: const Text('Kirim'),
)

_controller.text reads the value the user typed. Every manually created TextEditingController must be disposed of in dispose to prevent memory leaks.

GestureDetector and InkWell

For free-form interactions beyond buttons, use GestureDetector or InkWell — the latter provides the Material ripple effect:

Catching a tap gesture
InkWell(
  onTap: () {
    print('Ditekan');
  },
  child: const Padding(
    padding: EdgeInsets.all(8),
    child: Text('Ketik di sini'),
  ),
)

InkWell supports many gestures: onTap, onDoubleTap, and onLongPress. Use GestureDetector only when you don't need the Material effect. Another callback commonly used is onDoubleTap for secondary actions.

Form Validation and Controllers

Form with a Validator

Combine Form, TextFormField, and validator for centralized validation:

Form with email validation
final _formKey = GlobalKey<FormState>();
 
Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        decoration: const InputDecoration(labelText: 'Email'),
        validator: (value) {
          if (value == null || !value.contains('@')) {
            return 'Masukkan email yang valid';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            print('Form valid');
          }
        },
        child: const Text('Simpan'),
      ),
    ],
  ),
)

_formKey.currentState!.validate() runs all validators and returns true if they all pass. Validator errors are shown automatically below the field.

Simple Navigation and Routing

Moving Between Pages with Navigator

The simplest navigation uses Navigator.push with MaterialPageRoute:

Navigate to a new page
Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => const HalamanDetail(),
  ),
);

Navigator.push stacks a new page on top of the current one, complete with the default animation. Returning to the previous page is as simple as Navigator.pop(context).

Sending and Receiving Results

Receive a result from a new page
final hasil = await Navigator.push<String>(
  context,
  MaterialPageRoute(builder: (context) => const HalamanPilih()),
);
 
if (hasil != null) {
  print('Dipilih: $hasil');
}

The await Navigator.push and Navigator.pop(context, hasil) pattern is the standard way to send result data back — the foundation of the wizard flow we'll discuss in episode 11.

SnackBars, Dialogs, and Modal Bottom Sheets

SnackBar for Brief Feedback

Display a short notification with ScaffoldMessenger:

Showing a SnackBar
ScaffoldMessenger.of(context).showSnackBar(
  const SnackBar(
    content: Text('Perubahan tersimpan'),
    duration: Duration(seconds: 2),
  ),
);

A SnackBar appears at the bottom of the screen and disappears automatically. Wrap it in ScaffoldMessenger.of(context).showSnackBar so it binds correctly to the Scaffold.

Dialog and Modal Bottom Sheet

For confirmations, use a dialog; for contextual menus, use a bottom sheet:

A simple confirmation dialog
showDialog<void>(
  context: context,
  builder: (context) => AlertDialog(
    title: const Text('Hapus data?'),
    content: const Text('Tindakan ini tidak bisa dibatalkan.'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context, false),
        child: const Text('Batal'),
      ),
      TextButton(
        onPressed: () => Navigator.pop(context, true),
        child: const Text('Hapus'),
      ),
    ],
  ),
);

showDialog displays a modal dialog; the buttons in actions close the dialog by returning a value. The same pattern is used by showModalBottomSheet for panels that slide up from the bottom.

Conclusion

Key takeaways:

  • TextField with TextEditingController captures input; dispose the controller in dispose.
  • InkWell provides gestures with the Material effect; GestureDetector without it.
  • Form with validator gives consistent, centralized validation.
  • Navigator.push stacks pages; Navigator.pop(context, hasil) returns a result.
  • SnackBar for brief feedback; dialogs and bottom sheets for decisions.
  • Always validate input before sending data to the backend.

In the next episode 6 we discuss basic state management — managing local state with setState(), the lifting state up and prop drilling techniques, an introduction to the Provider pattern and InheritedWidget, and best practices for small-scale state. This is the bridge from individual widgets to a complete application.

Learn Flutter - Input, Forms, & Interaction | Learn Flutter