Logo
Back

Make a hero animation in Flutter

How to add a hero animation to your Flutter app? Why doesn't it work?

It is sometimes very useful to animate a piece of UI when transitioning between widgets.

Flutter's Hero widget can be used to provide an animation like this:

Hero animation example

To add it to your app, simply wrap your existing widget in Hero:

import 'package:flutter/material.dart';

class MenuImage extends StatelessWidget {
  final String photoUrl;
  final int itemId;

  const MenuImage({
    required this.itemId,
    required this.photoUrl,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Hero(
      tag: "menuItem$itemId",
      child: Image.network(
        photoUrl,
        fit: BoxFit.cover,
        semanticLabel: "$itemId's photo",
      ),
    );
  }
}

And make sure tag property is the same for both widgets!

(this code if from Make a food delivery app with DollarBackend (2/3))

Hero widget doesn't work

Here's an example when Hero widget doesn't work in Flutter (you can copy and run it on dartpad):

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: FirstWidget(),
    );
  }
}

class FirstWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Hero(
              tag: "image",
              child: Image.network("https://httpbin.org/image/jpeg"),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(
                      MaterialPageRoute(builder: (context) => SecondWidget()));
                },
                child: const Text("next"))
          ],
        ),
      ),
    );
  }
}

class SecondWidget extends StatefulWidget {
  @override
  State<SecondWidget> createState() => _SecondWidgetState();
}

class _SecondWidgetState extends State<SecondWidget> {
  String? imgUrl;

  @override
  void initState() {
    super.initState();

    Future.delayed(const Duration(milliseconds: 10), () {
      setState(() {
        imgUrl = "https://httpbin.org/image/jpeg";
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Stack(
          alignment: Alignment.topCenter,
          children: [
            if (imgUrl != null)
              Hero(
                tag: "image",
                child: Image.network(imgUrl!),
              ),
            const Align(
              alignment: Alignment.bottomCenter,
              child: Text("hero works!"),
            ),
          ],
        ),
      ),
    );
  }
}

As you can see, we're trying to switch from one page to another, but Hero widget doesn't work.

The reason being, it takes some time for imgUrl to become available on SecondWidget.

The widget gets rendered initially, there is no hero.
Animation starts, then imgUrl becomes available, so the widget gets re-rendered.

It looks like the animation doesn't work, but in reality the animation doesn't see another Hero on the first page.

How to fix

Make sure that you have a Hero widget on the first build.
For this example, passing imgUrl from one widget to another as a parameter resolves the problem.

Working code:


Having to debug both your app and your server is no fun!

We can host an open source backend for you! No need to set up and maintain your own server. Sign up here, no credit card required


import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: FirstWidget(),
    );
  }
}

const imgOne = "https://httpbin.org/image/jpeg";

class FirstWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Hero(
              tag: "image",
              child: Image.network(imgOne),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
                onPressed: () {
                  Navigator.of(context).push(
                    MaterialPageRoute(
                        builder: (context) => SecondWidget(imgUrl: imgOne)),
                  );
                },
                child: const Text("next"))
          ],
        ),
      ),
    );
  }
}

class SecondWidget extends StatefulWidget {
  String? imgUrl;

  SecondWidget({required this.imgUrl, super.key});

  @override
  State<SecondWidget> createState() => _SecondWidgetState();
}

class _SecondWidgetState extends State<SecondWidget> {
  String? _imgUrl;

  @override
  void initState() {
    super.initState();

    // _imgUrl should be set on the first run;
    _imgUrl = widget.imgUrl;

    Future.delayed(const Duration(milliseconds: 10), () {
      // and then it can be obtained from another source
      // (and probably in a higher resolution)
      setState(() {
        _imgUrl = "https://httpbin.org/image/png";
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Stack(
          alignment: Alignment.topCenter,
          children: [
            if (_imgUrl != null)
              Hero(
                tag: "image",
                child: Image.network(_imgUrl!),
              ),
            const Align(
              alignment: Alignment.bottomCenter,
              child: Text("hero works!"),
            ),
          ],
        ),
      ),
    );
  }
}
Blog
Writing cool articles about backends