Hero Widget in Flutter: Tutorial & Example

Updated: January 4, 2023 By: A Goodman Post a comment

This article is about the Hero widget and hero animations in Flutter. We’ll have a glance at the essence of those things and then examine a complete example of using them in practice.

The TL;DR

In simple words, a hero animation is an animation of an element of one page (screen) that flies to the other page when the user navigates. To set up a hero animation, what you need to do is to put 2 Hero widgets on 2 pages, like this:

// implement a Hero widget on source page
InkWell(
         // navigate to the detail page on tap
         onTap: () {
                  Navigator.of(context).push(/* destination route */);
         },
         // implement the Hero widget
         child: Hero(
                  tag: 'id-123',
                  child: /* ... */,
         ),
);

And this:

// implement the corresponding Hero widget on destination page
Hero(
        tag: 'id-123',
        child: /* ... */,
),

2 Hero widgets on 2 different pages are linked together simply because they have the same tag. This is very important. The tag can be an arbitrary Dart object, like a string, a number, etc.

The constructor of the Hero widget:

Hero({
  Key? key, 
  required Object tag, 
  CreateRectTween? createRectTween, 
  HeroFlightShuttleBuilder? flightShuttleBuilder, 
  HeroPlaceholderBuilder? placeholderBuilder, 
  bool transitionOnUserGestures = false, 
  required Widget child
})

There are 2 properties that mustn’t be left out:

  • tag: The identifier for the hero. The Hero widget’s tag on the departure page and that of the Hero widget on the destination page must be the same.
  • child: The widget that flies from one page to another page during the transition

A wall of text can be boring and make you get confused. Move on to the next section to get a better understanding by writing a working demo app.

Full Example

App Preview

The demo project we’re going to build consists of 2 pages:

  • Main screen: Presents a grid of thumbnail images. You can navigate to the detail page by tapping on a thumbnail.
  • Detail page: Displays detailed information about an image, including a full-size image and a title. You can go back to the main page by pressing the back button or the close button (these 2 buttons do the same thing but have different implementations)

The animated GIF screenshot below clearly depicts our final product. Turn your intention to the animations when navigating between the pages (the images are softly enlarged/reduced in size):

The Code

Below is the complete source code in main.dart with explanations in the comments. For simplicity’s sake, I put everything in a single file. However, you can keep the code well organized by placing each screen in a separate file (this isn’t a must-do but is considered good practice).

// main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // Remove the debug banner
      debugShowCheckedModeBanner: false,
      title: 'KindaCode.com',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const KindaCodeDemo(),
    );
  }
}

// implement the main page
class KindaCodeDemo extends StatefulWidget {
  const KindaCodeDemo({Key? key}) : super(key: key);

  @override
  State<KindaCodeDemo> createState() => _KindaCodeDemoState();
}

class _KindaCodeDemoState extends State<KindaCodeDemo> {
  // dummy data to feed the grid view
  // list of images
  final List<Map<String, dynamic>> _images = [
    {
      'id': 'i1',
      'title': 'One',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/1.png'
    },
    {
      'id': 'i2',
      'title': 'Two',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/2.png'
    },
    {
      'id': 'i3',
      'title': 'Three',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/3.jpg'
    },
    {
      'id': 'i4',
      'title': 'Four',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/4.jpg'
    },
    {
      'id': 'i5',
      'title': 'Five',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/5.jpg'
    },
    {
      'id': 'i6',
      'title': 'Six',
      'url': 'https://www.kindacode.com/wp-content/uploads/2022/08/6.jpg'
    }
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('KindaCode.com')),
      body: Container(
        padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
        // implement the grid view
        child: GridView.builder(
            gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
                maxCrossAxisExtent: 200,
                childAspectRatio: 3 / 2,
                crossAxisSpacing: 20,
                mainAxisSpacing: 20),
            itemCount: _images.length,
            itemBuilder: (BuildContext ctx, index) {
              return InkWell(
                key: ValueKey(_images[index]['id']),
                // navigate to the detail page on tap
                onTap: () {
                  Navigator.of(context).push(MaterialPageRoute(
                      // pass the image data to the detail page
                      builder: (context) => DetailPage(_images[index])));
                },
                // implement the Hero widget
                child: Hero(
                  // important: the tag
                  tag: _images[index]['id'],
                  // display the image
                  child: Image.network(
                    _images[index]['url'],
                    fit: BoxFit.cover,
                  ),
                ),
              );
            }),
      ),
    );
  }
}

// detail page
class DetailPage extends StatelessWidget {
  // get the image data that was passed from the main page
  final Map image;
  const DetailPage(this.image, {Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      // implement the app bar
      appBar: AppBar(
        backgroundColor: Colors.transparent,
        elevation: 0,
        title: Text(image['title']),
        actions: [
          // this button does the same thing as the back button
          // I put it here to show you how to go back to the previous screen by implementing your own code
          IconButton(
              onPressed: () {
                Navigator.of(context).pop();
              },
              icon: const Icon(Icons.close))
        ],
      ),
      body: Center(
        // implement the Hero widget
        child: Hero(
          // important: the tag
          tag: image['id'],
          child: Image.network(image['url']),
        ),
      ),
    );
  }
}

Conclusion

You’ve learned the fundamentals of the Hero widget, which is not so hard to implement but gives a fruitful result. Again, it might be superfluous, but it’s important to keep in mind that you have to set the tag for the Hero widget.

Flutter is amazing and can help us build more beautiful things in less time. Continue strengthening your skills by exploring the broad world of mobile development by taking a look at the following articles:

You can also tour around our Flutter topic page or Dart topic page for the most recent tutorials and examples.

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

Related Articles