A month of Flutter: the real hero animation

Abraham Williams - Dec 30 '18 - - Dev Community

For the last post before the month's wrap up tomorrow, I wanted to do something more fun: use a hero animation between the home page list and the individual post page.

When I first implemented the Hero animation it never worked going back from a PostPage to the HomePage. The reason was that HomePage would get rerendered and that would generate new fake posts. So I moved the fake data generation up a level to MyApp and pass it into HomePage. This is more realistic as going to the HomePage shouldn't request the Posts every time.

HomePage(
  title: 'Birb',
  posts: _loadPosts(context),
)
~~~{% endraw %}

The {% raw %}`PostPage`{% endraw %} implementation is a simple {% raw %}`StatelessWidget`{% endraw %} that takes {% raw %}`Post`{% endraw %} and renders a {% raw %}`PostItem`{% endraw %}. This will become more complex as things like comments and likes are implemented but works for now.{% raw %}

~~~dart
class PostPage extends StatelessWidget {
  const PostPage({
    Key key,
    @required this.post,
  }) : super(key: key);

  final Post post;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Post'),
        centerTitle: true,
        elevation: 0.0,
      ),
      body: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0),
          child: PostItem(post),
        ),
      ),
    );
  }
}
~~~{% endraw %}

With {% raw %}`PostItem`{% endraw %} being used to render on the {% raw %}`HomePage`{% endraw %} and on the {% raw %}`PostPage`{% endraw %}, wrapping the {% raw %}`Image`{% endraw %} in a {% raw %}`Hero`{% endraw %} is handled in a single place. {% raw %}`tag`{% endraw %} is how {% raw %}`Hero`{% endraw %} knows what to transition between pages.{% raw %}

~~~dart
Hero(
  tag: post.id,
  child: ClipRRect(
    child: Image.network(post.imageUrl),
    borderRadius: BorderRadius.circular(10.0),
  ),
)
~~~{% endraw %}

The last piece is navigating from {% raw %}`PostList`{% endraw %} to {% raw %}`PostPage`{% endraw %} when a user taps on a {% raw %}`PostItem`{% endraw %}. I'll handle this with an [{% raw %}`InkWell` widget](https://docs.flutter.io/flutter/material/InkWell-class.html) so there is a nice [Material ripple](https://material.io/design/motion/understanding-motion.html#usage).

~~~dart
InkWell(
  onTap: () => _navigateToPost(context, post),
  child: PostItem(post),
)
~~~

The navigation is more complex then [opening the registration page](https://bendyworks.com/blog/a-month-of-flutter-navigate-to-user-registration) for two reasons. [Named routes](https://flutter.io/docs/cookbook/navigation/named-routes) don't support parameters and I wanted a simple [transition](https://docs.flutter.io/flutter/widgets/PageRouteBuilder/buildTransitions.html) between the rest of the content on the page.

~~~dart
void _navigateToPost(BuildContext context, Post post) {
  Navigator.of(context).push(
    PageRouteBuilder<PostPage>(
      pageBuilder: (
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
      ) {
        return PostPage(post: post);
      },
      transitionsBuilder: (
        BuildContext context,
        Animation<double> animation,
        Animation<double> secondaryAnimation,
        Widget child,
      ) {
        return FadeTransition(
          opacity: animation,
          child: child,
        );
      },
    ),
  );
}
~~~{% endraw %}

Here I will [{% raw %}`push`{% endraw %}](https://docs.flutter.io/flutter/widgets/Navigator/push.html) a [{% raw %}`PageRouteBuilder`](https://docs.flutter.io/flutter/widgets/PageRouteBuilder-class.html) onto the navigation stack. `PageRouteBuilder` has two key builders in use here. `pageBuilder` builds the widget that should be rendered as the new page and `transitionBuilder` specifies how to transition between the old and new pages. Note that this [`FadeTransition`](https://docs.flutter.io/flutter/widgets/FadeTransition-class.html) is not related to implementing `Hero` earlier.

The tests for {% raw %}`PostPage`{% endraw %} is simple and just checking that {% raw %}`PostItem`{% endraw %} is rendered. I did update the {% raw %}`PostItem`{% endraw %} test to expect that its {% raw %}`Hero`{% endraw %} widget had the correct {% raw %}`tag`{% endraw %} value.{% raw %}

~~~dart
expect(tester.widget<Hero>(hero).tag, post.id);
~~~{% endraw %}

{% raw %}`PostsList`{% endraw %} tests had to be wrapped in a {% raw %}`MaterialApp`{% endraw %} as [{% raw %}`InkWell`{% endraw %}](https://docs.flutter.io/flutter/material/InkWell-class.html) must have a Material widget ancestor.

The navigation and animation from {% raw %}`PostsList`{% endraw %} to {% raw %}`PostPage`{% endraw %} is now doing more work so I replaced several [{% raw %}`pump`{% endraw %}](https://docs.flutter.io/flutter/flutter_test/WidgetTester/pump.html) pauses with [`pumpAndSettle`](https://docs.flutter.io/flutter/flutter_test/WidgetTester/pumpAndSettle.html).

Here is the fancy {% raw %}`Hero`{% endraw %} animation:

{% youtube 2u-_GOWhWNc %}

## Code changes

{% github https://github.com/abraham/birb/issues/68 %}
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .