DEV Community

Cover image for A month of Flutter: the real hero animation
Abraham Williams
Abraham Williams

Posted on • Originally published at bendyworks.com

6 1

A month of Flutter: the real hero animation

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

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more