Widgets Métodos vs Clases Statefulwidget/statelesswidget en Flutter, ¿Cuál es Mejor?
Índice de contenido
Quiero mostrarte cómo modularicé el sistema de Tabs (etiquetas) en mi aplicación de Academia en Flutter y compartir las claves de esta refactorización. Originalmente, el sistema estaba implementado con métodos, y lo migré a clases para mejorar la reusabilidad y la modularidad.
Estos posts son aspectos de buenas prácticas que considero y de que pocos hablan, antes vimos como crear una clases abstractas en StatefulWidget para heredar en Flutter.
️ Reusabilidad y Modularización
Aunque actualmente solo uso estas cinco pestañas en un módulo, es importante dejar el código lo más limpio posible para una futura escalabilidad. Te mostraré la implementación original (basada en métodos) y cómo la mejoré migrándola a clases, lo cual facilita el mantenimiento.
❌ Problema 1: Propiedades Mezcladas en el Mismo Nivel
En el enfoque basado en métodos, todas las propiedades necesarias para construir las cinco pestañas se declaraban en la misma clase _TabsDetailCourseState, lo que resultaba en un código poco claro.
Implementación Basada en Métodos (Problema)
La clase padre del Widget contenía todas las propiedades necesarias para todos los Tabs, haciendo muy difícil saber qué propiedad (loadComments, _commentEditId, _commentTitleController, etc.) era utilizada por cuál método (_contentTab1, _contentTab2, etc.).
class TabsDetailCourse extends StatefulWidget {
final TutorialModel tutorialModel;
String searchClass = '';
TabsDetailCourse(this.tutorialModel, {super.key});
@override
State<TabsDetailCourse> createState() => _TabsDetailCourseState();
}
class _TabsDetailCourseState extends State<TabsDetailCourse> {
int active = 1;
bool loadComments = false;
bool blockWriting = false;
late AppModel appModel;
late ThemeSwitch appTheme;
final List<ExpansionTileController> _listSectionsExpansionTileController = [];
final ScrollController _controller = ScrollController();
int _commentEditId = 0;
bool _sendMessage = false;
final _commentTitleController = TextEditingController();Estas propiedades son empleadas en los métodos para cada etiqueta:
@override
Widget build(BuildContext context) {
Widget content = _contentTab1();
// tabs de la app de detalle
switch (active) {
case 2:
content = _contentTab2(
widget
.tutorialModel
.tutorialSectionsModel[appModel.selectedSectionIndex]
.tutorialSectionClassesModel[appModel.selectedClassIndex]
.description,
);
break;
case 3:
content = _contentTab3();
break;
case 4:
content = _contentTab4();
break;
case 5:
content = _contentTab5(
'${LocaleKeys.creationDate.tr()} ${widget.tutorialModel.date}',
widget.tutorialModel.content,
"$baseUrlAcademy/free/${widget.tutorialModel.urlClean}",
widget.tutorialModel.title,
);
break;
}Por lo tanto, no sabemos a cual vamos a emplear cada una de esas propiedades, en el enfoque de clase, no tenemos esos problemas al tener las clases una mejor modularización:
class _TabsDetailCourseState extends State<TabsDetailCourse> {
// tab activa
int active = 1;
late AppModel appModel;
@override
void initState() {
appModel = Provider.of<AppModel>(context, listen: false);
super.initState();
}
@override
Widget build(BuildContext context) {
// secciones y clases del curso
Widget content = _Tab1(tutorialModel: widget.tutorialModel);
// tabs de la app de detalle
switch (active) {
case 2:
// Notas de la clase
content = _Tab2(tutorialModel: widget.tutorialModel);
break;
case 3:
// Notas del usuario por clases
content = _Tab3(tutorialModel: widget.tutorialModel);
break;
case 4:
// Comentarios de los estudiantes al curso
content = _Tab4(tutorialModel: widget.tutorialModel);
break;
case 5:
// Acerca del curso
content = Tab5(
duration:
'${LocaleKeys.creationDate.tr()} ${widget.tutorialModel.date}',
html: widget.tutorialModel.content,
urlShareTutorial:
"$baseUrlAcademy/free/${widget.tutorialModel.urlClean}",
titleTutorial: widget.tutorialModel.title,
);
break;
}En el código anterior, fíjate que no tenemos nada de esas propiedades simplemente el active que indica cuál etiqueta está activa cuál taque está activo y el modelo que es el provider para mí que siempre lo utilizo para ver las cositas ahí así de limpio. Por lo tanto, y aquí puedes ver una ganancia
Ahora, las propiedades específicas de cada Tab (como las de comentarios, etc.) se mueven a sus propias clases StatefulWidget o StatelessWidget. La clase principal solo retiene lo esencial: la pestaña activa (active) y los modelos de datos compartidos (appModel).
Al migrar a clases, logramos una mejor modularización. El código es mucho más limpio y fácil de leer, ya que cada Tab (clase) contiene solo las propiedades que necesita.
❌ Problema 2: Métodos al Mismo Nivel
Otro problema del enfoque basado en métodos es que cada método de Tab (_contentTab4()) a su vez necesitaba invocar varios métodos auxiliares (ej., _getCommentUsers()).
Al estar todos estos métodos auxiliares en el mismo nivel que los métodos principales, no se sabe con certeza:
- Qué métodos se utilizan directamente.
- Qué métodos son auxiliares de otros.
- Dónde se consumen.
Implementación Basada en Clases (Solución)
Al mover la lógica de cada Tab a su propia clase, todos sus métodos auxiliares quedan agrupados dentro de ese contexto de clase, resolviendo la ambigüedad.
// En el enfoque de Clases, todos los métodos (incluidos los auxiliares)
// forman parte de la clase del Tab, haciendo obvio dónde se usan.
class __Tab4State extends State<_Tab4> {
***
await _getCommentUsers();
_listComments();
setStateIfMounted(() {});
_commentEditId = 0;
_commentTitleController.text = '';
showToastMessage(context, LocaleKeys.messageSend.tr());
}
} Problema
Al estar todos los métodos a un mismo nivel y no agrupados en clases, no sabemos que métodos son empleados para ser consumidos directamente o indirectamente y donde se consumen, mientras que en una clase, ya sabemos que se emplean dentro de la misma al formar parte de la clase.
Widget _contentTab4() {
***
_getCommentUsers()
***
}❌ Problema 3: Asincronía al Devolver un Widget
El problema más complejo del enfoque de métodos es cuando un Tab necesita obtener datos de Internet de forma asíncrona para construirse.
En el enfoque de métodos, el método que devuelve el Widget necesita manejar la promesa (Future) del dato, lo que complica la implementación:
Widget _contentTab3() {
final comment = AcademyHelper.getNoteUserByClassGet(
widget
.tutorialModel
.tutorialSectionsModel[appModel.selectedSectionIndex]
.tutorialSectionClassesModel[appModel.selectedClassIndex]
.id,
appModel.userToken,
);
// establece el comentario
comment.then(
(c) =>
_controllerHtmlEditor.document = Document.fromDelta(htmlToDelta(c)),
);
}El uso de .then() o la necesidad de envolver todo en un FutureBuilder complica el mantenimiento.
Implementación Basada en Clases (Solución)
Al usar un StatefulWidget para cada Tab, podemos seguir el patrón de diseño clásico y limpio de Flutter:
- Definir la clase del Tab como un StatefulWidget.
- En el método initState(), iniciamos la obtención de los datos asíncronos.
- Cuando los datos están listos (dentro del Future), llamamos a setState() para redibujar solo ese Tab.
La clase Tab es síncrona, y la gestión de la asincronía queda contenida y controlada dentro de su propio State, sin afectar al componente principal.
✅ Conclusiones
A veces me critico y a veces me alegra ver estas implementaciones antiguas, ya que evidencian el crecimiento como desarrollador.
En muchos tutoriales (incluso en proyectos grandes), estos detalles de estructura se pasan por alto. Sin embargo, estas pequeñas decisiones, como priorizar las clases sobre los métodos para componentes modulares, marcan la diferencia entre un código pobre y un código mantenible y escalable.
La estructura mejorada, aunque siempre mejorable, nos permite reutilizar este sistema de Tabs en el futuro de una manera mucho más limpia que la implementación inicial.
Siguiente paso, aprende a crear tus primeros juegos en 2D con el motor de videojuegos de Flame y Flutter.
Acepto recibir anuncios de interes sobre este Blog.
Hablo sobre que debemos de emplear al momento de desarrolar widgets personalizados más complejos en Flutter.