A gesture is a user interaction with the screen.
In Flutter, most visual widgets (Container, Image, Text, Row) do not have an onPressed property. They are passive layout elements.
To bring them to life, we need to “wrap” them in special widgets capable of listening to the user. We have two main players: the invisible GestureDetector and the visual InkWell.
GestureDetector: detection without visual feedback
GestureDetector is a widget that paints nothing on the screen (it is invisible), but it can detect almost any finger movement over its child.
It is the Swiss Army knife of interactivity.
Detecting a tap with onTap
Suppose we have a blue Container and we want it to do something when tapped.
GestureDetector(
onTap: () {
print("You tapped the box!");
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text("Tap me")),
),
)
Advanced gestures
The power of GestureDetector is that it goes far beyond a simple click:
onDoubleTap: Quick double tap (like Instagram’s “like”).onLongPress: Long press (for context menus).onPanUpdate: Drag the finger (to move objects).onScaleUpdate: Pinch (to zoom).
GestureDetector(
onTap: () => print("Single tap"),
onDoubleTap: () => print("Double tap - Like!"),
onLongPress: () => print("Opening menu..."),
child: MyWidget(),
)
GestureDetector is purely logical. It detects the gesture, executes your code, but provides no visual feedback to the user. The button does not light up or change color. To the user, it might seem like the App has frozen if the action takes time to execute.
InkWell: Material visual feedback
If you use Material Design, you should provide visual feedback. The user needs to know that they have tapped the element.
InkWell is a widget that, when tapped, generates an “ink ripple” (Ripple Effect) that spreads from the finger.
InkWell(
onTap: () {
print("Ripple effect!");
},
child: Container(
padding: EdgeInsets.all(20),
child: Text("Flat button with effect"),
),
)
The opaque background problem
This is mistake #1 when using InkWell.
If you put an InkWell with a colored Container inside it… the ripple won’t show!
// ❌ BAD: The Container's color COVERS the InkWell's ripple
InkWell(
onTap: () {},
child: Container(
color: Colors.blue, // This is opaque and sits ON TOP of the ink
child: Text("You won't see the ripple"),
),
)
InkWell paints the ink on the nearest Material widget in the background. If your child Container has a color, it acts as an opaque paint layer on top of the ink.
The solution: Ink widget
To solve this, Flutter provides the Ink widget. It is like a Container, but specifically designed to work with InkWell and paint the ink on top of the background image or color.
// ✅ GOOD: Use the Ink widget for the layout
Ink(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
child: InkWell(
borderRadius: BorderRadius.circular(10), // So the ripple respects the borders
onTap: () {},
child: Container(
padding: EdgeInsets.all(20),
child: Text("Now the ripple is visible", style: TextStyle(color: Colors.white)),
),
),
)
Which widget to choose
The decision is straightforward based on user experience (UX):
| Widget | Recommended Usage | Example |
|---|---|---|
| InkWell | When the element looks like a button or a card and you want standard visual feedback. | List items, custom buttons, cards. |
| GestureDetector | When you need complex gestures or when the interaction should be subtle/invisible. | Zooming a photo, swiping to delete, pausing a video on tap. |
Swiping elements with Dismissible
A very common gesture on mobile is “swipe to dismiss”. Although we could build it with GestureDetector, Flutter gives us the Dismissible widget.
Dismissible(
key: Key('item_id'), // Required to identify what we are dismissing
background: Container(color: Colors.red), // What is shown while swiping
onDismissed: (direction) {
// Here we remove the item from our data list
setState(() {
myData.removeAt(index);
});
},
child: ListTile(title: Text("Swipe me to delete")),
)