Flutter: Extending State‹T›

Recently we’ve been exploring the ability to extend the base State<T> class, to add additional capabilities to our views, or writing our own custom Mixins to do the same.

Most Developers are familiar with using the various framework Mixins, like TickerProviderStateMixin but what is not commonly known, is how easy it is to create your own flavors of these. Additionally, many developers extend State<T> constantly, and write very repetitive boilerplate code, without realizing how easy it is to create their own BaseState<T> instead.

In this example, we’ll make a “base state” that provides 3 AnimatorControllers to any widget that needs to animate something. We could then apply this to any Widget in our application, and it automatically get 3 animators to play with. No setup, or teardown required.

This can be done by extending State<T> or by creating your own Mixin. We’ll demonstrate both (spoiler: they are extremely similar).

Extending State‹T›

The syntax for extending a State is very simple:

abstract class SomeState<T extends StatefulWidget> extends State<T> {
  @override
  void initState() {
    //Do some shared init stuff here
    super.initState();
  }
}

All the magic is in line 1 here. We declare as abstract so we don’t need to implement build(), we declare that T extends StatefulWidget and we extend State<T> that’s all we need to do! You can now override any of the lifecycle methods, like init, build, didUpdate or dispose.

Then, use it like this:

class SomeWidget extends StatefulWidget {
  @override
  _SomeWidgetState createState() => _SomeWidgetState();
}

class _SomeWidgetState<SomeWidget> extends SomeState {
   // No need to call init here, base-state is handling it
}

Notice that the only difference to a normal StatefulWidget, is that instead of using State<SomeWidget> we’re using SomeState<SomeWidget>. It’s just that easy!

So now lets look at a more interesting example. We’ll create a base state that sets-up and tears-down 3 animators.

abstract class MultiAnimationState<T extends StatefulWidget> extends State<T> with TickerProviderStateMixin {
  AnimationController anim1;
  AnimationController anim2;
  AnimationController anim3;

  @override
  void initState() {
    var defaultDuration = Duration(milliseconds: 350);
    anim1 = AnimationController(vsync: this, duration: defaultDuration);
    anim2 = AnimationController(vsync: this, duration: defaultDuration);
    anim3 = AnimationController(vsync: this, duration: defaultDuration);
    super.initState();
  }

  @override
  void dispose() {
    anim1.dispose();
    anim2.dispose();
    anim3.dispose();
    super.dispose();
  }

  // For fun, lets provide a bonus method, a shortcut for delayed methods.
  // It's likely we'll use these anims in a sequenced way and this makes it easier to read
  void delayed(double duration, VoidCallback action) async {
    // Do something in X seconds
    Future.delayed(Duration(milliseconds: (duration * 1000).round())).then((value) => action?.call());
  }
}

This will allow any StatefulWidget that uses MultiAnimationState to have easy robust animation. No need to nest your tree in a verbose TweenAnimationBuilder or write 15 lines of code each time you want to animate something. Maybe more importantly, it eliminates any potential errors related to not calling dispose().

In use it would look something like this:

class _SomePageWithAnimationsState extends MultiAnimationState<SomePageWithAnimations> {
  @override
  void initState() {
    super.initState();
    // We can access the properties in the parent class, and change the duration of any of the anims if we need.
    anim1.duration = Duration(seconds: 1);
    anim1.forward(); // Start Anim1
    delayed(.35, () => anim2.forward()); // Start Anim2 (delayed)
    delayed(.7, () => anim3.forward()); // Start Anim3 (delayed)
  }

  @override
  Widget build(BuildContext context) {
    // Anim1 fades in the whole view
    return FadeTransition(
      opacity: _anim1, 
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          // Content 1
          FadeTransition(opacity: _anim2, child: ...),
          // Content 2
          FadeTransition(opacity: _anim3, child: ...),
        ],
      ),
    );
  }
}

Pretty handy right? You could extend this to any common functionality in your app. Whether it’s setting up StreamBuilders, wiring global state or any number of applications, this can be a very simple and effective approach.

At some point though, inheritance will usually reach its limits, and you will find yourself needing something that lends itself better to Composition, and that’s where Mixins come in!

Using Mixins

If you’re coming from C#, Java or TypeScript, Mixins may seem foreign at first, but they are actually extremely simple. They do what they say, you can “mix in” any code into a class. The Mixin declares which class it is on and it can then access or override any of those class members and also add new fields and methods as needed.

What’s really nice about them is that they can be mixed and matched, ‘grafting’ just the behavior that you need onto a class. You don’t need to deal with massive base-classes, or confusing Heirarchy chains. The downside is that, because they all get mashed into the same class in the end, they share the same state. This can be a bit brittle and create issues the compiler can’t always spot (more on this later).

An example of a basic mixin on State<T> would look like this:

mixin SomeMixin<T> on State {
  @override
  void initState() {
    //Do some shared init stuff here
    super.initState();
  }
}

And then you can use it like this:

class SomeWidget extends StatefulWidget {
  @override
  _SomeWidgetState createState() => _SomeWidgetState();
}

class _SomeWidgetState<SomeWidget> extends State with SomeMixin {
   // No need to call init(), it's being handled by SomeMixin
}

To replicate our previous example, we can create a Mixin that manages the AnimatorControllers:

mixin MultiAnimationStateMixin on State, TickerProviderStateMixin {
  AnimationController anim1;
  AnimationController anim2;
  AnimationController anim3;

  @override
  void initState() {
    var defaultDuration = Duration(milliseconds: 350);
    anim1 = AnimationController(vsync: this, duration: defaultDuration);
    anim2 = AnimationController(vsync: this, duration: defaultDuration);
    anim3 = AnimationController(vsync: this, duration: defaultDuration);
    super.initState();
  }

  @override
  void dispose() {
    anim1.dispose();
    anim2.dispose();
    anim3.dispose();
    super.dispose();
  }

  // We can put our same Delayed function in a Mixin, just like we did the base-class, (just make sure no other mixin has declared one!)
  void delayed(double duration, VoidCallback action) async =>
      Future.delayed(Duration(milliseconds: (duration * 1000).round())).then((value) => action?.call());
}

And then, to use it, just do something like this:

class SomePageWithAnimations extends StatefulWidget {
  @override
  _SomePageWithAnimationsState createState() => _SomePageWithAnimationsState();
}

class _SomePageWithAnimationsState<SomePageWithAnimations> extends State
    with TickerProviderStateMixin, MultiAnimationStateMixin {
    // The rest of the code is identical to the inheritance approach!
}

Note that we declared our Mixin on both State and TickerProviderStateMixin this ensures that whenever we apply this Mixin, the Compiler throws an error if it does not also have TickerProviderStateMixin. Additionally, we can pass this into AnimationController.vsync, and it works because the Compiler knows that we will mixed with a TickerProvider at runtime! Pretty cool!

Limitations of Mixins

Mixins are pretty great, but they do suffer from the problem that they can fairly easily clash on field names or method definitions in unexpected ways. Something you need to be careful of. For example, if we tried to use 2 mixins, both declaring AnimatorController anim1 no errors will be thrown. One just replaces the other, and suddenly 2 mixins are unknowingly sharing the same state.

Generally speaking you could say that Mixins are highly effective at encapsulating logic and behavior, but they have a bit of a weakness in that they do not encapsulate their own state.

On the plus side, mixins are highly composable, easier to reason about than long hierarchy chains or a bloated base-class, and in practice variable name clashes tend to be fairly rare.

What’s Next

Due to the limitations with Mixins regarding state encapsulation, and the verbosity of using Builders, there is a growing movement in the community to get something like Hooks implemented into Flutter Core. Hooks offers all the same capabilities as State Mixins but with proper state encapsulation (and also no need to use a StatefulWidget!). If you have a few pounds of popcorn there is quite the discussion on the topic here: https://github.com/flutter/flutter/issues/51752

In a recent poll by Remi Rousselet (creator of Provider and Hooks), he asked the community if they would like this sort of API, the answer was pretty clear!

A non-verbose builder is essentially the same thing as a Hook, it would be able to hook into the widget lifecycle, be easily be composed in other Widgets, and fully encapsulate its own state. Here’s hoping the Flutter team hears everyone and releases something in the near future!

Check the Code

The examples used in this post are on GitHub: https://github.com/gskinnerTeam/flutter-demo-extending-state/blob/master/lib/main.dart . We hope this gives you more insight into extending State<T>, and a couple tools in your developer toolbox!

shawn.blais

Shawn has worked as programmer and product designer for over 20 years, shipping several games on Steam and Playstation and dozens of apps on mobile. He has extensive programming experience with Dart, C#, ActionScript, SQL, PHP and Javascript and a deep proficiency with motion graphics and UX design. Shawn is currently the Technical Director for gskinner.

@tree_fortress

Leave a Reply

Your email address will not be published. Required fields are marked *