Why you should avoid using Method Helpers in Flutter and use Widget classes instead.
Why should you avoid using Method Helpers in Flutter and use Widget classes instead?
Everything is a widget in Flutter, and we use widgets for everything from very easy components to very complex UI screens. However, developers commonly use method helpers for building UI components. I get it; I used to do it as well. They are easier to implement and, thus, faster to create complex UI elements. But we have to understand that this also comes with several downsides, especially performance issues.
In this article, I'm going to explain why using Method Helpers is not a good idea and why we should use Widget Classes instead.
Understanding Method Helpers and Widget Classes
Before diving into comparisons, I want all of us to be on the same page and explain what is a method helper in the context of Flutter.
So what's a Method Helper?
A method helper is a method that returns a widget, for example:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Favorite Books'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getItemOne(),
_getItemTwo(),
],
),
);
}
// This is a method helper
Widget _getItemOne() {
return const ListTile(
title: Text('The Great Gatsby'),
subtitle: Text('F. Scott Fitzgerald'),
leading: Icon(Icons.book),
trailing: Icon(Icons.favorite),
);
}
// This is another method helper
Widget _getItemTwo() {
return Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(10),
),
child: const Column(
children: [
Text('The Catcher in the Rye'),
Text('J.D. Salinger'),
],
),
);
}
If we refactor this code and create Widget classes instead, we can have the following:
class _FirstPageState extends State<FirstPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Favorite Books'),
),
body: const Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ItemOne(), // We have now a reference to a widget class
ItemTwo(),
],
),
);
}
}
class ItemTwo extends StatelessWidget {
const ItemTwo({
super.key,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(10),
),
child: const Column(
children: [
Text('The Catcher in the Rye'),
Text('J.D. Salinger'),
],
),
);
}
}
class ItemOne extends StatelessWidget {
const ItemOne({
super.key,
});
@override
Widget build(BuildContext context) {
return const ListTile(
title: Text('The Great Gatsby'),
subtitle: Text('F. Scott Fitzgerald'),
leading: Icon(Icons.book),
trailing: Icon(Icons.favorite),
);
}
}
We are writing a lot more code when creating Widget Classes. So, at first, using a method helper seemed more convenient because they are easier and faster to code, but there are some issues with Method Helpers:
Issues with method helpers
Lack of reusability and Scalability: Method helpers are usually not reusable outside the widget they are defined in, limiting their utility when we need to reuse them in other UI components.
Debugging and Testing Challenges: Debugging can also be more complicated because method helpers don't appear as a separe component in the widget tree, making them more difficult to be accesssed from a test.
Poor Performance issues: So here's the catch: Widgets that are returned from methods do not maintain their state and are rebuilt every time the parent widget rebuilds. Why this can be troublesome?
Suppose we have the following widget:
import 'package:flutter/material.dart';
class FirstPage extends StatefulWidget {
const FirstPage({super.key});
@override
State<FirstPage> createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
bool switchValue = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Favorite Books'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getBooks(),
],
),
);
}
Widget _getItemList({
required String bookName,
required String authorName,
required bool isFavorite,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 5),
child: Card(
child: ListTile(
title: Text(bookName),
subtitle: Text(authorName),
trailing: IconButton(
icon: Icon(isFavorite ? Icons.favorite : Icons.favorite_border),
onPressed: () {},
),
),
),
);
}
Widget _getBooks() {
return Expanded(
child: ListView(
children: [
_getItemList(
bookName: "Ender's Game",
authorName: "Orson Scott Card",
isFavorite: true),
_getItemList(
bookName: "The Hitchhiker's Guide to the Galaxy",
authorName: "Douglas Adams",
isFavorite: false),
_getItemList(
bookName: "The Hobbit",
authorName: "J. R. R. Tolkien",
isFavorite: true),
_getItemList(
bookName: "Station Eleven",
authorName: "Emily St. John Mandel",
isFavorite: true),
_getItemList(
bookName: 'The Nightingale',
authorName: 'Christin Hannah',
isFavorite: true),
],
),
);
}
Widget _toggleFavorite() {
return Switch(
value: switchValue,
onChanged: (value) {
setState(
() {
switchValue = value;
},
);
},
);
}
}
I know it could be a more practical Widget, but bear with me. We have a Method Helper that is called toggleFavorite(); if you paid more attention, you could see the following:
onChanged: (value) {
setState(
() {
switchValue = value;
},
);
},
That setState() method belongs to the parent Widget which we called FirstPage. Every time we're triggering this method, we are updating the closest Widget Class this Method Helper is implemented in, in this case the complete First Page widget, which contains some components that do not require to be updated. This can lead to performance issues in our Flutter application when we have a widget tree with multiple UI components.
If we refactor this code and use the following:
import 'package:flutter/material.dart';
class FirstPage extends StatefulWidget {
const FirstPage({super.key});
@override
State<FirstPage> createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('My Favorite Books'),
),
body: const Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ToggleFavorite(), // <- Using the ToggleFavorite widget
],
),
);
}
}
class ToggleFavorite extends StatefulWidget {
const ToggleFavorite({super.key});
@override
State<ToggleFavorite> createState() => _ToggleFavoriteState();
}
class _ToggleFavoriteState extends State<ToggleFavorite> {
bool switchValue = false;
@override
Widget build(BuildContext context) {
return Switch(
value: switchValue,
onChanged: (value) {
setState(
() {
switchValue = value;
},
);
},
);
}
}
Now the setState is isolated inside the ToggleFavorite Widget class that extends a Stateful Widget, updating only this isolated widget instead of the parent Widget, leading to a more performant application.
Now that we have a bit of a better understanding, we can list more benefits of using Widget Classes:
- Enhanced Reusability: By encapsulating our UI components, we have reuse them on different part of our applications.
- Easier Debugging and Testing: Since Widget Classes are isolated, create a Widget Test is also very easy as we only need the instance of the isolated widget and perform testing on it separately from other UI components.
- Improved Performance: Widget Classes mantain their state and context, which helps in optimizing the app performance overall.
Conclusion
So there you have it: performance issues are the main reason why we should avoid using Method Helpers. I understand that we are constantly under time pressure as developers and fall into doing what can be done faster. But by doing so, we can get ourselves into a more problematic situation that will be harder to solve if we keep doing this practice as the project grows. So remember always to use Widget Classes in order to create more performant applications. Thank you so much for reading this far, and let me know if you have any questions in the comment section.
Happy Coding. :D