Abstract classes in StatefulWidget and their use through inheritance in Flutter

Video thumbnail

I'm going to show you how you can implement class inheritance, specifically abstract classes, on a StatefulWidget in Flutter with Dart.

What's interesting or different from traditional inheritance is that, in Flutter, we have two classes:

  • The StatefulWidget class.
  • The state class, which is updated every time we run setState.

This is what makes it interesting how we can inherit this structure and how we should form it.

Comparison with traditional heritage

In any object-oriented language, this would look like this:

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!');
  }
}

We have a class marked as abstract.

We define a required method and other methods that we don't necessarily need to override.

When we inherit, we use overrides only on what we want to modify; everything else remains as is:

  @override
  void sound() {
    print('Cat: Miau!');
  }

This case is simple because it involves single inheritance.

But in the StatefulWidget, remember that the structure is different, as we'll see below.

Abstract class for StatefulWidgets

This is part of the academy application, specifically the viewer, where we have two main blocks:

  • The options panel (full screen, notes, zoom, etc.).
  • The content (in a native viewer or in a web viewer with WebView).
    • The native viewer translates HTML elements into widgets directly, while the web viewer loads HTML within a WebView.

Both share common characteristics, which is why it is advisable to use an inheritance structure with abstract classes.

Definition of the base class

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> {
  // full screen mode
  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();
  }
}

First, we create the BaseVisorPage class, which inherits from StatefulWidget.

This class is defined as abstract because:

  1. I don't want it to be instantiable directly.
  2. I only want to preserve the structure, not the complete behavior.

Then, in the state, we define the BaseVisorPageState class, also abstract.
This is where the interesting part comes in:

  • We use generics (T) to indicate that the state depends on the parent class.
  • T can be anything, but it must inherit from BaseVisorPage, since we need it to correspond to the same widget.
  • This ensures that the State<T> structure is maintained correctly.
  • Implementation of concrete viewers.

Now, its use is very simple and we must implement the methods that are not defined, such as buildBarOptions() and buildOptionsAndTitlesSection(); we can also take advantage of common behaviors between these classes, such as obtaining the data source using the getSectionActive() method:

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),
        ***
  }
}  

We can override methods to leverage their current functionality and add (in this example, fetching data) and add additional logic:

@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}';
 }
}

To do this, we call the super class to invoke either properties:

super.book

As in this example, it is the book property, which is necessary to create instances of these classes, or the aforementioned method:

await super.getSectionActive();

@override
getSectionActive() async {
  await super.getSectionActive();

  htmlNoteBook = await AcademyHelper.getBooksNoteGet(
    // bookSectionActive,
    bookSectionModel.id,
    appModel.userToken,
  );

  ***
}

Advantages of the scheme

The big advantage is that:

  • We have a modularized structure.
  • We reuse common code between viewers.
  • We can maintain shared behaviors (for example, getSectionActive) and override only what changes.
  • Thus, each viewer maintains its own specific features, but both inherit the foundation necessary to function.

Conclusion

This abstract class structure in StatefulWidget allows for the creation of clearer, reusable, and scalable components.

In my case, I apply it to the book viewer in the academic app, where I need:

  • Control sections, notes, zoom, full screen, etc.
  • Share logic between native and web viewers.

If you've used the app, you'll see that using the native viewer offers more options for widgets, such as changing the size or selecting text, than using the HTML viewer. However, both have the same structure and behaviors in common.

I agree to receive announcements of interest about this Blog.

I show you how to implement the two abstract classes that make up the StatefulWidget and how to use them through inheritance.

| 👤 Andrés Cruz

🇪🇸 En español