Implicit animations in Flutter: solved exercise

Implicit animations in Flutter: solved exercise

Implicit animations are the easiest to implement in Flutter: you change a value and the widget automatically animates the transition. No AnimationController or explicit Tween needed. The rule: if an AnimatedFoo exists for your Foo widget, use it.

Problem statement

Build a demo screen showing:

  1. AnimatedContainer: a card that changes size, color, and border radius.
  2. AnimatedOpacity: a panel that fades in and out.
  3. AnimatedSwitcher: a number that scales when it changes.
  4. TweenAnimationBuilder: a custom animated progress bar.

Dependencies

Flutter SDK only.

Full solution

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// lib/main.dart
import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: AnimationsDemo()));

class AnimationsDemo extends StatefulWidget {
  const AnimationsDemo({super.key});

  @override
  State<AnimationsDemo> createState() => _AnimationsDemoState();
}

class _AnimationsDemoState extends State<AnimationsDemo> {
  // AnimatedContainer state
  bool _expanded = false;

  // AnimatedOpacity state
  bool _visible = true;

  // AnimatedSwitcher state
  int _counter = 0;

  // TweenAnimationBuilder state
  double _targetProgress = 0.3;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Implicit animations')),
      body: ListView(
        padding: const EdgeInsets.all(20),
        children: [
          // ── 1. AnimatedContainer ─────────────────────────────────────────
          _SectionTitle('1. AnimatedContainer'),
          Center(
            child: AnimatedContainer(
              duration: const Duration(milliseconds: 400),
              curve: Curves.easeInOut,
              width: _expanded ? 280 : 140,
              height: _expanded ? 140 : 70,
              decoration: BoxDecoration(
                color: _expanded ? Colors.indigo : Colors.blue.shade200,
                borderRadius: BorderRadius.circular(_expanded ? 24 : 8),
                boxShadow: [
                  BoxShadow(
                    color: Colors.black.withOpacity(0.15),
                    blurRadius: _expanded ? 16 : 4,
                    offset: const Offset(0, 4),
                  ),
                ],
              ),
              child: Center(
                child: AnimatedDefaultTextStyle(
                  duration: const Duration(milliseconds: 400),
                  style: TextStyle(
                    color: Colors.white,
                    fontSize: _expanded ? 22 : 14,
                    fontWeight: FontWeight.bold,
                  ),
                  child: Text(_expanded ? 'Expanded!' : 'Tap to expand'),
                ),
              ),
            ),
          ),
          const SizedBox(height: 8),
          Center(
            child: ElevatedButton(
              onPressed: () => setState(() => _expanded = !_expanded),
              child: Text(_expanded ? 'Collapse' : 'Expand'),
            ),
          ),

          const SizedBox(height: 32),

          // ── 2. AnimatedOpacity ───────────────────────────────────────────
          _SectionTitle('2. AnimatedOpacity'),
          AnimatedOpacity(
            opacity: _visible ? 1.0 : 0.0,
            duration: const Duration(milliseconds: 500),
            child: Container(
              padding: const EdgeInsets.all(16),
              decoration: BoxDecoration(
                color: Colors.green.shade50,
                border: Border.all(color: Colors.green),
                borderRadius: BorderRadius.circular(12),
              ),
              child: const Row(
                children: [
                  Icon(Icons.check_circle, color: Colors.green),
                  SizedBox(width: 8),
                  Text('Panel with fade in/out'),
                ],
              ),
            ),
          ),
          const SizedBox(height: 8),
          Center(
            child: ElevatedButton(
              onPressed: () => setState(() => _visible = !_visible),
              child: Text(_visible ? 'Hide' : 'Show'),
            ),
          ),

          const SizedBox(height: 32),

          // ── 3. AnimatedSwitcher ──────────────────────────────────────────
          _SectionTitle('3. AnimatedSwitcher'),
          Center(
            child: AnimatedSwitcher(
              duration: const Duration(milliseconds: 300),
              transitionBuilder: (child, animation) => ScaleTransition(
                scale: animation,
                child: child,
              ),
              child: Text(
                '$_counter',
                key: ValueKey(_counter), // key is required to detect the change
                style: const TextStyle(fontSize: 64, fontWeight: FontWeight.bold),
              ),
            ),
          ),
          const SizedBox(height: 8),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton(
                icon: const Icon(Icons.remove),
                onPressed: () => setState(() => _counter--),
              ),
              const SizedBox(width: 16),
              IconButton(
                icon: const Icon(Icons.add),
                onPressed: () => setState(() => _counter++),
              ),
            ],
          ),

          const SizedBox(height: 32),

          // ── 4. TweenAnimationBuilder ─────────────────────────────────────
          _SectionTitle('4. TweenAnimationBuilder'),
          TweenAnimationBuilder<double>(
            tween: Tween(begin: 0, end: _targetProgress),
            duration: const Duration(milliseconds: 600),
            curve: Curves.easeOut,
            builder: (context, value, _) {
              return Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    '${(value * 100).toInt()}%',
                    style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
                  ),
                  const SizedBox(height: 6),
                  ClipRRect(
                    borderRadius: BorderRadius.circular(8),
                    child: LinearProgressIndicator(
                      value: value,
                      minHeight: 20,
                      backgroundColor: Colors.grey.shade200,
                      valueColor: AlwaysStoppedAnimation(
                        Color.lerp(Colors.red, Colors.green, value)!,
                      ),
                    ),
                  ),
                ],
              );
            },
          ),
          const SizedBox(height: 12),
          Slider(
            value: _targetProgress,
            onChanged: (v) => setState(() => _targetProgress = v),
          ),

          const SizedBox(height: 40),
        ],
      ),
    );
  }
}

class _SectionTitle extends StatelessWidget {
  final String text;
  const _SectionTitle(this.text);

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Text(
        text,
        style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
      ),
    );
  }
}

Expected result

  • The blue card smoothly expands to double size with rounded corners when the button is tapped.
  • The green panel fades in and out with AnimatedOpacity.
  • The number scales when it changes instead of cutting abruptly.
  • The progress bar fills with an animation and its color transitions from red to green.

Most-used implicit animation widgets

WidgetWhat it animates
AnimatedContainerAny Container property
AnimatedOpacityOpacity
AnimatedPaddingPadding
AnimatedAlignAlignment
AnimatedDefaultTextStyleText style in the subtree
AnimatedPositionedPosition in a Stack
AnimatedSwitcherTransition between different widgets
TweenAnimationBuilderAny value with customizable interpolation

When to use explicit animations

If you need:

  • Fine cycle control (pause, reverse, loop): AnimationController.
  • Coordinated animations across multiple widgets: AnimatedBuilder.
  • Physics-based animations (spring, bounce): SpringSimulation.

Common mistakes

  • AnimatedSwitcher without a ValueKey: if the new widget has the same type as the previous one, Flutter reuses the element and does not animate the transition.
  • Changing curve without enough duration: curves like Curves.bounceOut need sufficient duration or the bounce gets clipped.
  • TweenAnimationBuilder with a tween that always creates a new instance: move the Tween outside build or use begin: null to animate from the previous value.

Practical use

Implicit animations turn a flat UI into a fluid experience with no extra complexity. Use them by default; switch to explicit animations only when you need control that implicit widgets do not provide.

Guided practice and next step

FAQ

Do implicit animations affect performance?

Minimally. Flutter optimizes repaints to only the changed area. If many cards animate simultaneously, make sure each AnimatedContainer is in a separate widget to isolate rebuilds.

Can I combine AnimatedContainer with GestureDetector?

Yes. AnimatedContainer is a regular widget; wrap it in GestureDetector or InkWell to make the card tappable.

Does AnimatedSwitcher work with any widget?

Yes, but each entering and leaving widget must have a different Key (usually ValueKey) so Flutter detects the change and applies the transition.