Flutter User Interaction: Buttons, TextFields & Gestures | Ch. 9

Flutter Course Chapter 9: User Interaction, showing illustrations of buttons, text input fields, and hand gestures tapping a mobile screen.


Welcome to Chapter 9! We have come a long way. We started with the basics of Dart (Chapter 2), set up our professional environment (Chapter 3), learned the theory of State (Chapter 6), mastered layout (Chapter 7), and added content like Text and Images (Chapter 8).

But so far, our apps have been pretty quiet. They sit there and look pretty, but they don't do anything. They don't respond when you touch them. They don't let you type.

It's time to add Interactivity.

In this chapter, we are going to cover the three primary ways a user talks to your app:

  1. Buttons: The standard way to trigger an action.
  2. TextFields: The standard way to accept text input.
  3. GestureDetector: The "magic wrapper" that turns any widget into a button.

By the end of this chapter, you'll be able to build a functional Login Screen UI that accepts an email and password and reacts when the user taps "Login".

1. The Button Family

Flutter used to have buttons with names like `RaisedButton` and `FlatButton`. These are deprecated (dead). Do not use them.

Modern Flutter uses three main button widgets that share a similar design philosophy. They all take two required parameters:

  1. `child`: The content of the button (usually a `Text` or `Icon`).
  2. `onPressed`: The function to run when the button is tapped.

ElevatedButton (The "Main" Button)

This is your primary "Call to Action" button. It has a background color and a slight drop shadow, making it look "elevated" off the screen.


ElevatedButton(
  onPressed: () {
    print('Button Pressed!');
  },
  child: Text('Submit'),
)

TextButton (The "Flat" Button)

This button has no visible background or border. It looks like clickable text. It is commonly used for less important actions like "Cancel" or "Learn More" inside a dialog box.


TextButton(
  onPressed: () {
    print('Cancel Pressed!');
  },
  child: Text('Cancel'),
)

OutlinedButton (The "Bordered" Button)

This button has a border but no background color (it is transparent). It sits nicely between the other two in terms of visual importance.


OutlinedButton(
  onPressed: () {
    print('More Info Pressed!');
  },
  child: Text('More Info'),
)

The `onPressed` Callback & Disabling Buttons

The `onPressed` property takes a function. You can pass an anonymous function `() { ... }` or a named function.

How to Disable a Button: In Flutter, you don't set `isEnabled = false`. Instead, you simply set `onPressed` to `null`.


ElevatedButton(
  // If this is null, the button automatically looks "greyed out"
  // and cannot be clicked.
  onPressed: null, 
  child: Text('Disabled Button'),
)

This is a great example of "UI = f(state)". If your form isn't valid, your state should reflect that, and you pass `null` to the button. When the form becomes valid, you pass a real function, and the button lights up.

Styling Buttons (`styleFrom`)

Styling buttons can be complex because they handle their own states (hovered, pressed, disabled). To make it easier, Flutter provides a static method called `styleFrom`.


ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.purple, // Background color
    foregroundColor: Colors.white, // Text/Icon color
    elevation: 10, // Shadow size
    padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20), // Rounded corners
    ),
  ),
  child: Text('Styled Button'),
)

2. TextField: Getting User Input

To get text from the user, we use the `TextField` widget.

The Basic `TextField`

At its simplest, you just drop it in.


TextField()

This gives you a line that brings up the keyboard when tapped.

Decorating the Input (`InputDecoration`)

Just like `Text` has `TextStyle`, `TextField` has `InputDecoration`. This is where you add labels, hints, and icons.


TextField(
  decoration: InputDecoration(
    labelText: 'Email Address', // Moves up when you type
    hintText: 'name@example.com', // Disappears when you type
    prefixIcon: Icon(Icons.email), // Icon on the left
    suffixIcon: Icon(Icons.check), // Icon on the right
    border: OutlineInputBorder(), // Nice box border
  ),
)

Handling Keyboards (`keyboardType` & `obscureText`)

You can help the user by showing the right keyboard for the job.

  • `keyboardType`:
    • `TextInputType.emailAddress`: Shows the `@` sign.
    • `TextInputType.phone`: Shows a number pad.
    • `TextInputType.multiline`: Allows multiple lines of text.
  • `obscureText`: Set this to `true` for passwords. It turns the text into dots.

TextField(
  keyboardType: TextInputType.emailAddress,
  obscureText: false, // Set true for password
)

3. Retrieving the Text (The Controller Pattern)

This is the most critical part. How do you actually get what the user typed?

You could use the `onChanged` callback, which runs every time a character is typed. But for forms, there is a better, professional way: the `TextEditingController`.

What is a `TextEditingController`?

It is an object that "watches" a `TextField`. It always knows the current text value. You create it, attach it to the TextField, and then you can ask it for the text whenever you want (like when a button is pressed).

How to use it (Step-by-Step)

Because a Controller needs to live and die with the widget, you must use a `StatefulWidget`.

Step 1: Create the Controller variable.


class _MyFormState extends State<MyForm> {
  // 1. Create the controller
  final TextEditingController _emailController = TextEditingController();

Step 2: Attach it to the TextField.


  @override
  Widget build(BuildContext context) {
    return TextField(
      controller: _emailController, // 2. Attach it here
      decoration: InputDecoration(labelText: 'Email'),
    );
  }

Step 3: Read the value.


  ElevatedButton(
    onPressed: () {
      // 3. Read the value using .text
      print("User typed: ${_emailController.text}");
    },
    child: Text('Submit'),
  )

Step 4: Dispose of it (Important!).

Controllers stay in your phone's memory even if the widget is closed. This is called a "Memory Leak." To prevent this, you must override the `dispose` method and clean it up.


  @override
  void dispose() {
    // 4. Clean up the controller when the widget is removed
    _emailController.dispose();
    super.dispose();
  }
}

4. GestureDetector: Making Anything Tappable

Buttons are great, but sometimes you want to tap an Image, a Container, or a Custom Card.

Why `Container` doesn't have `onTap`

You might try to type `Container(onTap: ...)` and fail. Why? Because `Container` is a layout widget, not an interaction widget. Flutter separates responsibilities.

Using `GestureDetector`

To make any widget interactive, simply wrap it in a `GestureDetector`.


GestureDetector(
  onTap: () {
    print("Box tapped!");
  },
  onDoubleTap: () {
    print("Box double tapped!");
  },
  onLongPress: () {
    print("Box long pressed!");
  },
  child: Container(
    color: Colors.blue,
    width: 100,
    height: 100,
    child: Center(child: Text("Tap Me")),
  ),
)

It's that easy. It's invisible, adds no visual layout change, but adds interaction capabilities to its child.

`InkWell`: The "Ripple" Effect

`GestureDetector` is invisible. When you tap it, nothing visual happens (no "click" effect).

If you want the Material Design "Ripple" effect (that splash of ink when you tap), use `InkWell` instead. It works exactly like `GestureDetector`, but it must have a `Material` widget as an ancestor to draw the ink on.

5. Mini-Project: Building a Login Screen

Let's put it all together. We will build a simple login form with an Email field, a Password field, and a Login button. When the button is pressed, we'll print the credentials to the console.

Create a new file (or use `main.dart`) and try this code:


import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(home: LoginScreen()));
}

class LoginScreen extends StatefulWidget {
  @override
  _LoginScreenState createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  // 1. Create Controllers
  final TextEditingController _emailController = TextEditingController();
  final TextEditingController _passwordController = TextEditingController();

  @override
  void dispose() {
    // 2. Dispose Controllers
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Login Demo')),
      body: Padding(
        padding: EdgeInsets.all(20.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // Email Field
            TextField(
              controller: _emailController,
              keyboardType: TextInputType.emailAddress,
              decoration: InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.email),
              ),
            ),
            SizedBox(height: 20), // Spacer
            
            // Password Field
            TextField(
              controller: _passwordController,
              obscureText: true, // Hide password
              decoration: InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
                prefixIcon: Icon(Icons.lock),
              ),
            ),
            SizedBox(height: 20), // Spacer
            
            // Login Button
            ElevatedButton(
              onPressed: () {
                // 3. Retrieve Data
                String email = _emailController.text;
                String password = _passwordController.text;
                
                print("Logging in with: $email / $password");
              },
              style: ElevatedButton.styleFrom(
                minimumSize: Size(double.infinity, 50), // Full width button
              ),
              child: Text('LOGIN'),
            ),
          ],
        ),
      ),
    );
  }
}

Conclusion

You now have the power of interaction. Your apps are no longer static pictures; they are tools that can collect data and respond to the user.

We covered:

  • The modern button trio: ElevatedButton, TextButton, OutlinedButton.
  • How to collect text with TextField.
  • The professional TextEditingController pattern.
  • How to make anything clickable with GestureDetector.

In the next chapter, we are going to tackle one of the most common UI patterns in mobile apps: Lists. We'll learn how to display long lists of data that scroll, using ListView and GridView.

Comments