Clases abstractas en StatefulWidget y su uso mediante la herencia en Flutter

Video thumbnail

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:

  1. No quiero que se pueda instanciar directamente.
  2. 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.

| 👤 Andrés Cruz

🇺🇸 In english