Clases abstractas en StatefulWidget y su uso mediante la herencia en Flutter
Índice de contenido
Te voy a mostrar cómo puedes implementar la herencia de clases, específicamente clases abstractas, sobre un StatefulWidget en Flutter con Dart.
Lo interesante o lo diferente respecto a la herencia tradicional es que, en Flutter, tenemos dos clases:
- La clase del StatefulWidget.
- La clase del estado, que se va actualizando cada vez que hacemos el setState.
Esto es lo que hace interesante cómo podemos heredar esta estructura y cómo debemos formarla.
Comparación con la herencia tradicional
A diferencia del esquema tradicional, aquí le pedí a Gemini que me generara un ejemplo sencillo.
En cualquier lenguaje orientado a objetos, esto se vería así:
abstract class Animal {
void hacerSonido();
void moving() {
print('Moving');
}
}
class Dog extends Animal {
@override
void sound() {
print('Dog: Guau!');
}
}
class Cat extends Animal {
@override
void sound() {
print('Cat: Miau!');
}
}
Tenemos una clase marcada como abstracta.
Definimos un método obligatorio y otros métodos que no necesariamente debemos sobrescribir.
Cuando heredamos, usamos override solo en lo que queremos modificar; lo demás queda tal cual:
@override
void sound() {
print('Cat: Miau!');
}
Este caso es sencillo porque se trata de una herencia simple.
Pero en el StatefulWidget, recordemos que la estructura es distinta, como veremos a continuación.
Clase abstracta para los StatefulWidget
Esto forma parte de la aplicación de academia, concretamente del visor, donde tenemos dos bloques principales:
- El panel de opciones (fullscreen, notas, zoom, etc.).
- El contenido (en un visor nativo o en un visor web con WebView).
- El visor nativo traduce elementos HTML a widgets directamente, mientras que el visor web carga HTML dentro de un WebView.
Ambos comparten características en común, y por eso conviene usar una estructura heredada con clases abstractas.
Definición de la clase base
abstract class BaseVisorPage extends StatefulWidget {
final BookModel book;
const BaseVisorPage({super.key, required this.book});
// @override
// BaseVisorPageState createState();
}
abstract class BaseVisorPageState<T extends BaseVisorPage> extends State<T> {
// modo pantalla completo
bool fullScreen = false;
***
// other properties
late AppModel appModel;
// Estos métodos deben ser implementados por las subclases
// opciones de la barra
Widget buildBarOptions();
// titulos del libro
Widget buildOptionsAndTitlesSection(BuildContext context);
@override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
return CustomScaffold(
titleAppBar,
Stack(
children: [
***
],
),
);
}
// Trae la sección del libro activa
getSectionActive() async {
// seccion del libro
bookSectionModel = await AcademyHelper.getSectionByBook(
widget.book.id,
bookSectionActive,
sizeText.toInt(),
appModel.userToken,
);
}
@override
void dispose() {
***
super.dispose();
}
}
Primero, creamos la clase BaseVisorPage, que hereda de StatefulWidget.
Esta clase se define como abstracta porque:
- No quiero que se pueda instanciar directamente.
- Solo quiero conservar la estructura, no el comportamiento completo.
Luego, en el estado, definimos la clase BaseVisorPageState, también abstracta.
Aquí es donde entra lo interesante:
- Usamos genéricos (T) para indicar que el estado depende de la clase padre.
- T puede ser cualquier cosa, pero debe heredar de BaseVisorPage, ya que necesitamos que corresponda al mismo widget.
- Con esto logramos que la estructura de State<T> se mantenga correctamente.
- Implementación de visores concretos.
Ahora, su uso es muy sencillo y debemos de implementar los métodos que no se encuentran definidos como buildBarOptions() y buildOptionsAndTitlesSection(); también, podemos aprovechar comportamientos comunes entre estas clases como lo seria, obtener la fuente de datos mediante el método getSectionActive():
class VisorPageWeb extends BaseVisorPage {
static const ROUTE = "/visor";
const VisorPageWeb({required super.book});
@override
State<VisorPageWeb> createState() => _VisorPageWebState();
}
class _VisorPageWebState extends BaseVisorPageState<VisorPageWeb> {
WebViewController? _webViewController;
@override
void initState() {
super.initState();
appModel = Provider.of<AppModel>(context, listen: false);
***
}
@override
SingleChildScrollView buildOptionsAndTitlesSection(BuildContext context) {
return SingleChildScrollView(
child: Card(
margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
***
}
}
Podemos sobrescribir métodos para aprovechar su funcionamiento actual y agregar (en este ejemplo, obtener datos) y agregar lógica adicional:
@override
getSectionActive() async {
await super.getSectionActive();
htmlNoteBook = await AcademyHelper.getBooksNoteGet(
// bookSectionActive,
bookSectionModel.id,
appModel.userToken,
);
titleAppBar = bookSectionModel.title;
if (bookSectionActive == 0 && widget.book.image != '') {
// image = '${BlogHelper.baseUrlBookImage}${book.path}/${book.image}';
bookSectionModel.content =
'<img src="${BlogHelper.baseUrlBookImage}${widget.book.path}/${widget.book.image}" />${bookSectionModel.content}';
}
}
Para eso, hacemos llamados a la super clase, para invocar ya sea propiedades:
super.book
Como en este ejemplo es el de la propiedad libro, necesaria para poder crear instancias de estas clases, o del método mencionado:
await super.getSectionActive();
@override
getSectionActive() async {
await super.getSectionActive();
htmlNoteBook = await AcademyHelper.getBooksNoteGet(
// bookSectionActive,
bookSectionModel.id,
appModel.userToken,
);
***
}
Ventajas del esquema
La gran ventaja es que:
- Tenemos una estructura modularizada.
- Reutilizamos código común entre visores.
- Podemos mantener comportamientos compartidos (ejemplo: getSectionActive) y sobrescribir solo lo que cambia.
- Así, cada visor mantiene sus particularidades, pero ambos heredan la base necesaria para funcionar.
Conclusión
Este esquema de clases abstractas en StatefulWidget permite crear componentes más claros, reutilizables y escalables.
En mi caso lo aplico al visor de libros en la aplicación de academia, donde necesito:
- Controlar secciones, notas, zoom, fullscreen, etc.
- Compartir lógica entre visores nativo y web.
Ya que, si haz empleado la app, veras que cuando se emplea el visor nativo, existen más opciones sobre los widgets como variar el tamaño o seleccionar texto, a diferencia del visor HTML, pero, por más que sea, ambos tienen una misma estructura y comportamientos en comunes.
Acepto recibir anuncios de interes sobre este Blog.
Te muestro como implementar las dos clases abstractas que conforman el StatefulWidget y su uso mediante la herencia.