Flutter:页面跳转时Hero动画失效,仅返回时生效问题求助
I've run into this exact issue before! The problem here is that when you navigate to the detail page, the Hero widget on that page hasn't been built yet. Let me break it down:
Your list page's Hero is rendered once the FutureBuilder finishes loading products, so it's ready when you tap to navigate. But the detail page's Hero is wrapped inside its own FutureBuilder—meaning it only gets created after the single-product API call completes. By that time, the navigation transition has already started, and Flutter can't find a matching Hero tag on the destination page, so the animation skips entirely. The return animation works because the list page's Hero is already loaded and waiting.
Here are two solid fixes, depending on your needs:
Fix 1: Pass the Product Data Directly (Recommended)
Since your list page already has the full product data from the API, there's no need to make a second call for the same item. Just pass the entire product object to the detail page—this lets you render the Hero immediately, no waiting for API responses.
Step 1: Update the navigation in all_products.dart
Change your onTap to pass the full product instead of just the ID:
onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => SingleProduct(product: snapshot.data[index]), ), ); },
Step 2: Rewrite single_product.dart to use the passed data
You can simplify this to a StatelessWidget now, since you don't need to fetch data:
import 'package:flutter/material.dart'; class SingleProduct extends StatelessWidget { final Map<String, dynamic> product; const SingleProduct({Key? key, required this.product}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Products"), ), body: Container( color: Colors.white, child: Column( children: [ Hero( tag: product["id"], child: Container( color: Colors.transparent, child: Center( child: Image.network(product["image"], height: 200, width: 200), ), ), ), Expanded( child: Container( color: Colors.transparent, child: Card( color: Colors.white, elevation: 20.0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(20), topLeft: Radius.circular(20), ), ), child: Container( padding: EdgeInsets.all(10.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( product["title"], textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "\$: ${product["price"]}", style: TextStyle(fontSize: 16), ), Row( children: [ Text( product["rating"]["rate"].toString(), style: TextStyle(fontSize: 16), ), Icon(Icons.star, color: Colors.yellow, size: 20), ], ), ], ), SizedBox(height: 5), Text( "Category: ${product["category"]}", textAlign: TextAlign.left, style: TextStyle(fontSize: 16), ), SizedBox(height: 5), Text( product["description"], textAlign: TextAlign.justify, style: TextStyle(fontSize: 16), ), ], ), ), Align( alignment: Alignment.bottomCenter, child: Container( padding: EdgeInsets.all(15), decoration: BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.circular(50), color: Colors.black, ), height: 50, width: 130, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon(Icons.shopping_cart, color: Colors.white), Text("Add to cart", style: TextStyle(color: Colors.white)), ], ), ), ), ], ), ), ), ), ), ], ), ), ); } }
This not only fixes the Hero animation but also cuts down on unnecessary API calls—win-win!
Fix 2: Add a Placeholder Hero (If You Must Fetch Data)
If you need to fetch fresh data for the detail page (e.g., for real-time updates), you can render a placeholder Hero that matches the size of your list page's Hero before the API call completes. This gives Flutter a matching tag to animate to immediately.
Update single_product.dart like this:
import 'package:flutter/material.dart'; import 'package:httprequest/services/api_services.dart'; class SingleProduct extends StatefulWidget { final int id; SingleProduct(this.id); @override _SingleProductState createState() => _SingleProductState(); } class _SingleProductState extends State<SingleProduct> { Future? product; @override void initState() { product = ApiServices().getSingleProduct(widget.id); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Products"), ), body: FutureBuilder( future: product, builder: (context, AsyncSnapshot snapshot) { // Always render the Hero first, even before data loads return Container( color: Colors.white, child: Column( children: [ Hero( tag: widget.id, child: Container( color: Colors.transparent, height: 200, width: 200, child: snapshot.hasData ? Image.network(snapshot.data["image"], height: 200, width: 200) : // Match the list page's image container style here Container(color: Colors.grey[200]), ), ), // Show content once data is ready, else loading indicator snapshot.hasData ? Expanded( child: Container( color: Colors.transparent, child: Card( color: Colors.white, elevation: 20.0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(20), topLeft: Radius.circular(20), ), ), child: Container( padding: EdgeInsets.all(10.0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( snapshot.data["title"], textAlign: TextAlign.center, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), SizedBox(height: 5), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( "\$: ${snapshot.data["price"]}", style: TextStyle(fontSize: 16), ), Row( children: [ Text( snapshot.data["rating"]["rate"].toString(), style: TextStyle(fontSize: 16), ), Icon(Icons.star, color: Colors.yellow, size: 20), ], ), ], ), SizedBox(height: 5), Text( "Category: ${snapshot.data["category"]}", textAlign: TextAlign.left, style: TextStyle(fontSize: 16), ), SizedBox(height: 5), Text( snapshot.data["description"], textAlign: TextAlign.justify, style: TextStyle(fontSize: 16), ), ], ), ), Align( alignment: Alignment.bottomCenter, child: Container( padding: EdgeInsets.all(15), decoration: BoxDecoration( shape: BoxShape.rectangle, borderRadius: BorderRadius.circular(50), color: Colors.black, ), height: 50, width: 130, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Icon(Icons.shopping_cart, color: Colors.white), Text("Add to cart", style: TextStyle(color: Colors.white)), ], ), ), ), ], ), ), ), ), ) : Expanded(child: Center(child: CircularProgressIndicator())), ], ), ); }, ), ); } }
This way, the Hero exists on both pages during the navigation transition, so the animation works as expected.
Either approach should fix your issue. The first one is cleaner and more efficient, so I'd go with that unless you have a specific reason to refetch the product data.
内容的提问来源于stack exchange,提问作者Shanmukha

