Flutter User Interaction: Buttons, TextFields & Gestures | Ch. 9
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:
- Buttons: The standard way to trigger an action.
- TextFields: The standard way to accept text input.
- 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".
📖 Chapter 9: Table of Contents
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:
- `child`: The content of the button (usually a `Text` or `Icon`).
- `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
TextEditingControllerpattern. - 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
Post a Comment