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