Level up Flutter page transition: choreographing animations across screens

Shared elements transition is the go-to choice to spice up default page transition. With Flutter, we can simply achieve this using Hero
widget with just few additional lines of code.
But, little did we know that we can create slick page transition effects by carefully chaining each core component together!
Today, we will explore some neat screen transitions/animations through creating an interesting UI design I came across in Dribbble (Credited to Igor S.).

The core mechanics of the screen transitions involve:
- “Persistent”
Appbar
across screens - Tweening text size with
Hero
across screens - Choreographing animations across screens with
Navigator
Here’s a sneak peek of the end result.

I will not cover how to layout the general structure of the screens. But here’s a summary of what each screen comprised of:
- Main Screen: Scaffold with AppBar and ListView (consisted of header and list items).
- Detail Screen: Scaffold with AppBar and ListView (consisted of header, stack to arrange the image & title layout, Column to wrap description & icons).
1. “Persistent” AppBar
across screens
Usually we use MaterialPageRoute
/CupertinoPageRoute
to navigate to a new screen, depending on the platform we targeted.
We are granted a Fade Upwards Page Transition by default. But this is not what we wanted. AppBar ain’t “persistent”!

We can achieve the desired “persistent” AppBar effect by customizing our own PageRoute
. Rather, creating an illusion that the AppBar is “persistent”.
First, we extend PageRoute
and name it FadePageRoute
.
To speed up, you can keep the implementation of MaterialPageRoute<T>
. The only critical methods/properties we need to pay attention to are:
buildTransition
method: This method defines how the next screen shows up and leaves the screen. We will create a separate transitionFadeInPageTransition
to fit our needs.transitionDuration
properties: You can control the duration it takes to complete the screen transitions.
In FadeInPageTransition
, we take few parameters in constructor:
Animation<double> routeAnimation
: Animation which will be used during route transition. It plays from 0.0 to 1.0 duringNavigator.push
, and 1.0 to 0.0 duringNavigatorNavigastor.pop
.Widget child
: In short, our next screenDetailPage()
.
We then wrap our child with FadeTransition
, taking an opacity animation with respect to routeAnimation
to create a fade in effect.
And voilà, we created an effect as though the AppBar is “persistent” across screens.
This is also why I insisted to wrap quotation marks around the word persistent. If you can’t create genuine static AppBar
, you trick their eyes instead (evil grin).

2. Tweening text size with Hero
across screens
We will use Hero
widget for shared elements transition.
It’s pretty straightforward, just wrap the widget with Hero
and assigned same tag
across screens. The framework will complete the remaining heavy-lifting for you. Done and dusted!


Sadly, the result is kinda glitchy for Text
widget. This is not okay!
In fact, this is the expected behavior with the default implementation of Hero
widget.
Slightly in-depth explanation of why this could happen:
Hero
computes the size of source hero widget and size of destination hero widget.Hero
createsRectTween
to animate the Widget from size of source to the size of destination.Hero
animates the destination hero widget from source location and size to where it supposed to be in the next screen (which is why we see big text appears immediately during enter transition, and it slowly enlarges to the size of its destination).

In short, the default Hero
widget is not designed to animate text size and widget size during screen transition in the first place. But it offers a way for us to achieve the desired effect, through flightShuttleBuilder
.
flightShuttleBuilder
defines the widget when it “fly” across screen. By default, if nothing is provided for flightShuttleBuilder
, Hero
automatically uses our destination hero widget to fly across the screens.
To solve this, we will need to customized our own flightShuttleBuilder
, providing an animated Text
widget in the Hero
constructor.
We will focus on Title (highlighted in green in the previous image) for this example.
The Title widget is shared across screens, only varied with different font sizes according to the requirements (static | animating | enlarged on main screen | shrunk on detail screen). Therefore, we need to define the state to choreograph each case accordingly.
First, create an enum ViewState
as a mean to choreograph the widget.
Second, we create a StatefulWidget
for our Title that takes in ViewState
as parameter, so it behaves accordingly with theViewState
passed in.
Third, we define how the widget should behave according to each ViewState
. I defined the animation logics in initState
because I wanted the widget to animate immediately when it is instantiated.
Lastly, create your flightShuttleBuilder
that emits desired ViewState
according to the HeroFlightDirection
. Therefore, when the shared elements transition is triggered, our Text
widget will fly to its destination and update its text size concurrently.
This is it. We apply the same logic to the app header and icon.
Hooray! The Title is sized properly with the transition now!

P.S.: Alternatively, you can also pass in animation
from flightShuttleBuilder
as parameters to the animated StatefulWidget
, then use animation.drive(yourTween)
to synchronize the animations. I supposed this is a cleaner method to achieve the same effect. Please feel free to share your solution with me.
3. Choreographing animations across screens with Navigator
If you look at the AppBar
carefully, the hamburger menu icon animates to back icon on entering into new screen, and vice versa.
We can animate the AnimatedIcon
simply using a pre-defined animation, and call it before pushing new screen.
Easy peasy lemon squeezy!

OH WAIT, the AnimatedIcon
doesn’t animate back to hamburger when the detail screen pops! ΣΣ(゚Д゚;)

How do we choreograph animation to get back to its initial state when screen pops? And if possible, can we expand the trick to other use cases?
ValueNotifier
to the rescue!
We need to keep a variable that hold information across screens. The ValueNotifier
wrapping that variable will then react according to the variable whenever it is updated.
First, we define a variable bool returnFromDetailPage
as a trigger when we enter a new route or return from a route. Wrap the variable in ValueNotifier
so that we can listen to the variable when it updates according to the navigation.
Second, when we pop the screen on top of the stack, we return a value to the ValueNotifier
. The ValueNotifier
will then trigger its listener to animate accordingly.
Here we go! Our menu iconAppBar
animates as desired.


You can apply the same logic to trigger some fancy animations tailored to the data returned from other screen. Sweet!
P.S.: Alternatively, I hypothesized we can achieve the same effect by overriding buildTransitions
method in PageRoute<T>
, then utilize the secondaryAnimation
parameter from the function to choreograph the desired animation. Yet to be proven but I believe it works.
That’s all. We translated the UI design into a fully functional Flutter app (though not 100% alike).
The complete source code is hosted on GitHub.
Hope you found this interesting. Feel free to leave a comment and your thoughts!
Caveat: There are always better ways to achieve the similar/better results. If you have or found one, please share it to me and other readers. That’s the beauty of programming, isn’t it? :)