Skip to main content
Back to Fix Lab Hub
Verified: Flutter 3.24.3 • Dart 3.5.3
Motion & IdentityFX05Canonical Technical Guide

FX05: AnimatedSwitcher Missing Identity Key

Differentiate child widgets during transitions with unique keys so AnimatedSwitcher triggers

APIs:AnimatedSwitcherValueKeyKeyWidget.canUpdate

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.

Root Cause Analysis

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.

Verified Correction Rule

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 Identity
// Flutter Framework element update invariant:
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType && oldWidget.key == newWidget.key;
}
❌ Missing Key (canUpdate == true)
oldWidget: Container (key: null)
newWidget: Container (key: null)
Result: canUpdate returns TRUE
Action: Mutates element in-place
Visual: Snaps instantly without transition

AnimatedSwitcher only cross-fades when old and new children differ in identity.

✅ ValueKey Assigned (canUpdate == false)
oldWidget: Container (key: ValueKey(0))
newWidget: Container (key: ValueKey(1))
Result: canUpdate returns FALSE
Action: Spawns new element; retires old element
Visual: Smooth entrance & exit cross-fade

Distinct keys force Flutter to maintain both widgets in the tree during the transition animation.

Evidence Mode: Live Flutter Engine
Width:
Correction Strategy:

ValueKey<int>(_counter) triggers distinct element identity

✓ ValueKey changes trigger smooth cross-fade
Duration:
Real Flutter Web CanvasKit Engine320pxlight

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.

Open in Studio
Key Architectural Modifications:
  • 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
standalone_fx05_fixed_320px.dartFlutter 3.24.3 • Zero External Dependencies • Runnable
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)'),
        ),
      ],
    );
  }
}