Flutter Heroes and Villains — bringing balance to the Flutterverse.
A story about how heroes and villains work.
Villains allow you to add page transition like the one above with just a few lines of code.
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.
We’ll take a quick look at how the heroes are implemented.
There are three major steps involved in the hero animation.
- 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.
It is possible to observe the events of routes being pushed and popped with a
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 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.
Elements (concrete widgets) are placed in a tree. Using a visitor, you can walk down the tree and gather information.
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
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.
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
Defining a custom ticker is pretty easy. All there is to it is implementing the
TickerProvider interface and returning a new
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
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
It gets handed the child of the villain and this 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:
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:
..to = Duration(milliseconds: 150),
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.
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.