Buttons and Handling Taps
The built-in button widgets, and using GestureDetector when you need custom tap behavior.
2 phút đọc
Every interactive app needs a way to respond to taps. Flutter provides a family of ready-made button widgets for common cases, plus GestureDetector for when you need to make an arbitrary widget respond to touch.
The standard buttons
ElevatedButton(
onPressed: () {
print('Saved!');
},
child: const Text('Save'),
)
TextButton(
onPressed: () {},
child: const Text('Cancel'),
)
OutlinedButton(
onPressed: () {},
child: const Text('Learn more'),
)
IconButton(
icon: const Icon(Icons.favorite_border),
onPressed: () {},
)These differ mainly in visual weight, matched to how important the action is: ElevatedButton (filled, raised) for the primary action on a screen, OutlinedButton for a secondary action, TextButton for a low-emphasis action like "Cancel," and IconButton for icon-only controls. Picking the right one is as much a design decision as a code one — using ElevatedButton for every action on a screen makes none of them stand out.
Disabling a button
Every button's onPressed accepts null to disable it — Flutter handles the disabled visual style (grayed out, non-interactive) automatically:
ElevatedButton(
onPressed: isFormValid ? _submit : null,
child: const Text('Submit'),
)This pattern — a ternary that swaps between a callback and null — is the standard way to make a button's enabled state depend on other state in your widget, without an if/else branching into two separate widget trees.
GestureDetector for custom tap targets
Sometimes you want to make something other than a button respond to touch — a whole card, an image, a custom-drawn shape. GestureDetector wraps any widget and adds gesture callbacks without changing how that widget looks:
GestureDetector(
onTap: () {
print('Card tapped');
},
onLongPress: () {
print('Card long-pressed');
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
child: const Text('Tap this whole card'),
),
)Unlike the button widgets, GestureDetector provides zero visual feedback on its own — no ripple, no color change on tap. If you want a Material-style ripple effect on a custom tappable area, reach for InkWell instead, which behaves like GestureDetector but paints the standard tap ripple:
InkWell(
onTap: () {},
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.all(16),
child: const Text('Tap with a ripple'),
),
)A tappable counter, end to end
Putting a button together with the StatefulWidget pattern from an earlier lesson:
class LikeButton extends StatefulWidget {
const LikeButton({super.key});
@override
State<LikeButton> createState() => _LikeButtonState();
}
class _LikeButtonState extends State<LikeButton> {
bool _liked = false;
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
_liked ? Icons.favorite : Icons.favorite_border,
color: _liked ? Colors.red : Colors.grey,
),
onPressed: () => setState(() => _liked = !_liked),
);
}
}Every tap flips _liked and calls setState, which triggers a rebuild that picks a different icon and color based on the new value — the same describe-the-new-state pattern from the StatefulWidget lesson, just wired to a real tap instead of a plain button label.