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 Post
s every time.
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.
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.
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 so there is a nice Material ripple.
InkWell(
onTap: () => _navigateToPost(context, post),
child: PostItem(post),
)
The navigation is more complex then opening the registration page for two reasons. Named routes don't support parameters and I wanted a simple transition between the rest of the content on the page.
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
a PageRouteBuilder
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
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.
expect(tester.widget<Hero>(hero).tag, post.id);
PostsList
tests had to be wrapped in a MaterialApp
as InkWell
must have a Material widget ancestor.
The navigation and animation from PostsList
to PostPage
is now doing more work so I replaced several pump
pauses with pumpAndSettle
.
Here is the fancy Hero
animation: