Beyond the Basics: 5 Rules for onTap events in Flutter to outshine others

Atomic improvements for exceptional returns

Chahat Gupta
4 min readSep 30, 2023
Photo by cottonbro studio: https://www.pexels.com/photo/liking-a-photo-on-instagram-5052877/

The more you code and collaborate, the more you believe in improving tiny things. I love to read and write blogs that focus on atomic improvement.

One such tiny yet abundant thing is onTap — something that we all use, on every screen. This one is purely about those onTap events: The DOs and DON’Ts.

Rule #1: Widgets should not implement onTap logic

As the name suggests, a widget is a chunk of UI drawn on the screen, and it should know nothing about the business logic. If and when needed, it may pass events to its parent. The best way to do this is using functions as constructor parameters.

Takeaway: Do not create an anonymous function inside a widget to write business logic. Instead, pass the onTap even out of the widget and let the parent handle it.

// DON'T

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () {
debugPrint('MyWidget onTap called');
fetchFromServer();
},
child: Container(
width: 100,
height: 100,
color: Colors.orange,
),
);
}
// DO

class MyWidget extends StatelessWidget {

const MyWidget({this.onTap});

final void Function()? onTap;

@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Container(
width: 100,
height: 100,
color: Colors.orange,
),
);
}

Rule #2: onTap functions should be nullable

As a chunk of UI, a widget should be reusable. Different use cases may or may not require its onTap functionality. Because the primary reason of its existence is to get drawn on the screen rather than sending events to its parent or controller, it should be able to exist with or without its onTap event.

Takeaway: Make all event functions coming from a widget nullable.

// DON'T

class MyWidget extends StatelessWidget {

const MyWidget({required this.onTap});

final void Function() onTap;

@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Container(
...
),
);
}
// DO

class MyWidget extends StatelessWidget {

const MyWidget({this.onTap});

final void Function()? onTap;

@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
child: Container(
...
),
);
}

Rule #3: UI must know absolutely nothing about logic, not even in indicative sense

One common mistake even after using best practices is naming UI-event functions indicative to business logic. A widget inside a banking app should be unaware that it belongs to a banking app and have just enough data to draw itself and its children. In other words, a widget should be beautiful and foolish.

Takeaway: Name UI-event functions indicative to the event, and not to the underlying business logic. Naming such a function as onTapRegisterButton() is better than register().

// DON'T

class MyScreen extends StatelessWidget {

...

@override
Widget build(BuildContext context) => Scaffold(
body: MyWidget(
onTap: controller.fetchData,
);
);
}
// DO

class MyScreen extends StatelessWidget {

...

@override
Widget build(BuildContext context) => Scaffold(
body: MyWidget(
onTap: controller.onTapMyWidget,
);
);
}

class MyScreenController {

...

void onTapMyWidget() {
_fetchData()
}

void _fetchData() {
...
}

}

Rule #4: Pass models wherever possible

This one is not limited to UI but to all functions in general. Whenever you have to pass some data as a parameter, try to pass the whole model instead of just an ID or name. This is a good practice to minimise code changes in future as the business logic expands or changes.

Takeaway: Pass models to functions as parameters instead of IDs.

// DON'T

void onTapMyWidget(int subjectId) {
...
}
// DO

void onTapMyWidget(Subject subject) {
...
}

Rule #5: Always specify HitTestBehavior

When using GestureDetector for clicks, don’t forget to add behavior to your widget. This property specifies how hits (clicks) propagate to child widgets. You will be using HitTestBehavior.opaque in most cases but I suggest you should check this short description on flutter.dev for your knowledge.

Takeaway: Have absolute control over your widget’s children by specifying its hit behavior.

class MyWidget extends StatelessWidget {

...

@override
Widget build(BuildContext context) => GestureDetector(
onTap: onTap,
behavior: HitTestBehavior.opaque,
child: Container(
...
),
);
}

Conclusion

Small things like these define your proficiency. The interesting aspect of such small details is their abundance. You’ll encounter onTap events in hundreds of places within a codebase. Enhancing them can have a significant positive impact on your code’s maintainability and the end-user experience. Let me know your thoughts on this topic, and let’s meet in another blog. Happy coding!

🚀 Let’s connect!

Connect with me on LinkedIn for professional insights and networking.

Explore my contributions on GitHub and Stack Overflow to collaborate on something amazing! ✨

Consider clapping 👏🏼 if you found this article insightful.

--

--

Chahat Gupta
Chahat Gupta

Written by Chahat Gupta

Mobile Tech Lead specialising in Android, iOS, and Flutter. Sharing insights and learnings on mobile development to inspire and elevate tech professionals.

Responses (5)