Flutter Heroes and Villains — bringing balance to the Flutterverse.

A story about how heroes and villains work.

Villains combined with a hero.

Villains allow you to add page transition like the one above with just a few lines of code.

The package is available here. You can find how to use villains in the projects README. This post is more focused on explaining heroes and villains and the thought process behind all of it.

One of the most awesome parts of flutter is having such a nice and clean API for everything. I just love the way you can use Heroes. Two simple lines of code, and it just works. You just drop in Heroes at two places, assign according tags and the rest is taken care of.


Before you understand the villain, you must first understand the hero.

Simple Hero transition.

We’ll take a quick look at how the heroes are implemented.

Overview

There are three major steps involved in the hero animation.

  1. Finding and matching the heroes

The first step is to determine which heroes exist and which have the same tag.

2. Determining the hero positions

Then, the position of both heroes are captured and the flight is ready to happen.

3. Initiating the flight

The flight always happens on the new screen, but not with the actual widget. The widget on the opening page is replaced with an empty placeholder widget (SizedBox) during the flight. Instead the Overlay is used (the Overlay can display widgets on top of everything).

The whole hero animation happens on the page being opened. The widgets don’t share any state between pages and are completely separate.

The NavigationObserver

It is possible to observe the events of routes being pushed and popped with a NavigationObserver .

HeroController

The hero uses this class to start the flight. Besides being able to add NavigationObservers yourself, the MaterialApp adds the HeroController by default. Take a look.

The Hero Widget

The constructor of the Hero

The hero widget doesn’t actually do much. It holds the child and the tag. In addition to that, the createRectTween argument determines what route the Hero takes while flying to its destination. The default implementation is the MaterialRectArcTween. As the name suggests it moves the hero along an arc to its final position.

The state of the hero also is responsible for capturing sizes and replacing itself with the placeholder.

_allHeroesFor

Elements (concrete widgets) are placed in a tree. Using a visitor, you can walk down the tree and gather information.

heroes.dart

An inline function called visitor is declared inside the method. The context.visitChildElements(visitor) method and element.visitChildren(vistor)call the function until all elements below the context are visited. On each visit it checks whether that child is a Hero and if it is, it saves it into a map.

The start of the flight

heroes.dart

This gets called in response to the route push/pop event. On line 14 and 15 you can see the _allHeroesFor call which finds all heroes on both pages. Starting on line 21 a _HeroFlightManifest is constructed and the flight is initiated. From here on there is a bunch of code setting up the animation and handling edge cases. I encourage you to take a look at the whole class, it’s pretty interesting and there’s a lot to learn from it. Also you can take a look at this.


How do villains work

Villains are simpler creatures than Heroes.

Hero with 3 villains playing (AppBar, Text, FAB).

They use the same mechanic of finding all villains for a given context and they also use a NavigationObserver to automatically react on page transitions. But instead of animating from one screen to another they only animate on their respective screen.

SequenceAnimation and custom TickerProvider

When dealing with animations, you usually use the SingleTickerProviderStateMixin or the TickerProviderStateMixin. In this case the animation doesn’t start in a StatefulWidget and we therefore need another way to access a TickerProvider.

Defining a custom ticker is pretty easy. All there is to it is implementing the TickerProvider interface and returning a new Ticker.

First all villains which should not be playing (those who set animateExit/animateEntrance to false) are filtered out. Then an AnimationController with the custom TickerProvider is created. Using the SequenceAnimation library each Villain is assigned an animation running from 0.0–1.0 in their respective time slot (the from and to duration). At the end, animations are all started. When all of them finish, the controller is disposed.

The Villain build()

This might look scary, but bear with me. Let’s just look at line 3 and 4. The widget.villainAnimation.animatedWidgetBuilder is a custom typedef:

typedef Widget AnimatedWidgetBuilder(Animation animation, Widget child);

Its job is to return a widget which animates according to the animation (most of the times the returned widget is an AnimatedWidget).

It gets handed the child of the villain and this animation:

widget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation)

The chain method first evaluates the CurveTween. It then uses that value to evaluate the animatable on which it is called. This simply adds the desired curve to the animation.

That was a rough overview on how the villains work, be sure to also look at the source code and feel free to ask questions.


Mutable static variables are bad, let me explain..

I was sitting at my desk in the late evening, writing tests. After a few hours every single test was passing and it seemed there were no bugs. Right before heading to bed I ran all the tests together to make sure it’s alright. Then this happend:

Each test passes on its own but not together

I was pretty confused. Every test succeed before. Sure enough, when I ran those two tests by themselves, they worked as expected. But when running all tests together the last two failed. WTF.

The first reaction was obviously: “My code must be working, it has to do something with the way tests are executed! Maybe tests are playing in parallel and thus interfering with each other? Maybe it is because I used the same keys?”.

Brian Egan point out to me that deleting one particular test fixed the bug and moving it to the top made all other tests fail too. If that doesn’t scream “SHARED DATA” then I don’t know what does.

When I found out what the problem was I just couldn’t stop laughing. It was exactly the reason why using static variables are considered bad in certain situations.

Basically, the predefined animations were all static. I was too lazy to write a method for each animation taking all the parameters that a VillainAnimation needs. So I made the VillainAnimation mutable (bad idea). This way I didn’t have to explicitly write all the necessary parameters into the method. This looked like this when using it:

Villain(
villainAnimation: VillainAnimation.fromBottom(0.4)
..to = Duration(milliseconds: 150),
child: Text("HI"),
)

The test which broke everything was supposed to test villain transitions starting after the page transition finished. It set the starting point of the animation to 1 second. Because it was setting it on a static reference, the test after that used that as the default. The tests failed because an animation can’t run from 1 second to 750 milliseconds.

The fix was pretty easy (making everything immutable and passing the arguments in the method) but I still found this little bug quite entertaining.


Wrapping up

Thanks to the Villains the balance between good and evil is now restored.

Opinions and discussions about the #fluttervillains are welcome. If you create cool animations with the villains, I’d love to see it.

My Twitter: @norbertkozsir