FX05: AnimatedSwitcher Missing Identity Key
Differentiate child widgets during transitions with unique keys so AnimatedSwitcher triggers
Observed Symptom & Flutter Error Assertion
AnimatedSwitcher fails to animate when state changes, snapping instantly instead.
LOG DIAGNOSTIC: AnimatedSwitcher did not initiate child cross-fade transition.
Reason: The incoming child widget matches the existing child's runtimeType and key (null == null).
Flutter element reconciliation reuses the existing RenderObject in place, skipping insertion/removal animations.Declared Conditions
Switching between two child widgets of the same runtimeType (e.g. Text or Container) inside AnimatedSwitcher without assigning distinct ValueKey parameters.
Official References
Widget.canUpdate checks `oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key`. When keys are omitted, Flutter reconfigures the existing element in-place instead of creating a new element, bypassing the entrance/exit animation.
Assign a distinct Key (e.g. `ValueKey<int>(_count)`) to each child widget so Flutter recognizes it as a new element and initiates the transition.
Flutter Element Reconciliation: Widget.canUpdate Contract
R05 AnimatedSwitcher & Element IdentityAnimatedSwitcher only cross-fades when old and new children differ in identity.
Distinct keys force Flutter to maintain both widgets in the tree during the transition animation.
ValueKey<int>(_counter) triggers distinct element identity
Corrected: ValueKey Assigned to Child
Assign `key: ValueKey<int>(_counter)` to the child widget. Flutter recognizes the new identity and triggers the animated cross-fade transition.
- ✓Add key: ValueKey<int>(_counter) to the AnimatedSwitcher child
- ✓Widget.canUpdate returns false due to mismatched keys
- ✓AnimatedSwitcher schedules exit transition for old element and entrance transition for new element
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: Scaffold(body: Center(child: SwitcherDemo()))));
class SwitcherDemo extends StatefulWidget {
const SwitcherDemo({super.key});
@override
State<SwitcherDemo> createState() => _SwitcherDemoState();
}
class _SwitcherDemoState extends State<SwitcherDemo> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 400),
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(opacity: animation, child: child);
},
child: Container(
key: ValueKey<int>(_counter),
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
decoration: BoxDecoration(
color: const Color(0xFF2DD4BF),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Text(
'Count: $_counter',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () => setState(() => _counter++),
icon: const Icon(Icons.touch_app),
label: Text('Increment (Animates with ValueKey)'),
),
],
);
}
}