A month of Flutter: the real hero animation
Tags: flutter, tutorial, animation, navigation
Published: 2018-12-30 21:56:30

For the last post before the month's wrap up tomorrow, I wanted to do something more fun: use a [hero animation](https://flutter.io/docs/development/ui/animations/hero-animations) 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](https://bendyworks.com/blog/a-month-of-flutter-real-faker-data). 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 `Post`s every time.

~~~dart
HomePage(
  title: 'Birb',
  posts: _loadPosts(context),
)
~~~

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

~~~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),
        ),
      ),
    );
  }
}
~~~

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

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

The last piece is navigating from `PostList` to `PostPage` when a user taps on a `PostItem`. I'll handle this with an [`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,
        );
      },
    ),
  );
}
~~~

Here I will [`push`](https://docs.flutter.io/flutter/widgets/Navigator/push.html) a [`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 `PostPage` is simple and just checking that `PostItem` is rendered. I did update the `PostItem` test to expect that its `Hero` widget had the correct `tag` value.

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

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

The navigation and animation from `PostsList` to `PostPage` is now doing more work so I replaced several [`pump`](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 `Hero` animation:

{% youtube 2u-_GOWhWNc %}

## Code changes

{% github https://github.com/abraham/birb/issues/68 %}