İçeriğe geç / Skip to content / Zum Inhalt
Ahmet Balaman LogoAhmet Balaman

Flutter: Maximale Wirkung ohne Aufwand mit impliziten Animationen

Ahmet Balaman
FlutterAnimationAnimatedContainerImplicit AnimationUI/UX

Denken Sie bei Animationen an komplexe Formeln, AnimationController und seitenweise Code? Was, wenn ich Ihnen sage, dass sich Ihre Widgets schon durch eine einzige Änderung geschmeidig animieren lassen?

Genau das leisten die impliziten Animationen von Flutter. Wenn Sie setState aufrufen, wechselt Ihr Widget automatisch vom alten zum neuen Wert. Magie? Nein, Flutter-Ingenieurskunst.

Explizit oder implizit: Wo liegt der Unterschied?

Es gibt zwei Arten von Animationen:

Explizite Animationen

  • Erfordern einen AnimationController
  • Werden eingesetzt, wenn Sie volle Kontrolle wollen
  • Mehr Code, mehr Flexibilität
  • Beispiel: komplexe, verkettete Animationen

Implizite Animationen

  • Kein Controller nötig
  • Für 90 % der Fälle ausreichend
  • Weniger Code, tolle Ergebnisse
  • Beispiel: Buttonfarbe, Größenänderung, Positionswechsel

Goldene Regel: Wenn Sie die Animation nicht von Anfang bis Ende steuern müssen, nehmen Sie implizite Animationen.

Live-Demo

Sie können dieses Widget im interaktiven Beispiel unten ausprobieren:

💡 Falls das Beispiel oben nicht lädt, klicken Sie auf DartPad, um es in einem neuen Tab auszuführen.

AnimatedContainer: das Schweizer Taschenmesser

AnimatedContainer ist die animierte Variante des Container. Sobald Sie eine Eigenschaft ändern, animiert er automatisch.

Grundlegende Verwendung

class AnimatedBoxDemo extends StatefulWidget {
  @override
  _AnimatedBoxDemoState createState() => _AnimatedBoxDemoState();
}

class _AnimatedBoxDemoState extends State<AnimatedBoxDemo> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: GestureDetector(
        onTap: () {
          setState(() {
            _isExpanded = !_isExpanded;
          });
        },
        child: AnimatedContainer(
          duration: Duration(milliseconds: 300),
          width: _isExpanded ? 200 : 100,
          height: _isExpanded ? 200 : 100,
          decoration: BoxDecoration(
            color: _isExpanded ? Colors.blue : Colors.red,
            borderRadius: BorderRadius.circular(_isExpanded ? 50 : 10),
          ),
          child: Center(
            child: Text(
              'Tap Me',
              style: TextStyle(color: Colors.white),
            ),
          ),
        ),
      ),
    );
  }
}

Ein einziger setState-Aufruf animiert drei Eigenschaften gleichzeitig:

  • Breite und Höhe
  • Farbe
  • Eckenradius

Was kann AnimatedContainer?

AnimatedContainer kann alle Eigenschaften des Container animieren:

1. Größenanimation

AnimatedContainer(
  duration: Duration(seconds: 1),
  width: isLarge ? 300 : 100,
  height: isLarge ? 300 : 100,
  child: FlutterLogo(),
)

2. Farbübergang

AnimatedContainer(
  duration: Duration(milliseconds: 500),
  color: isActive ? Colors.green : Colors.grey,
  child: Text('Status'),
)

3. Padding-Animation

AnimatedContainer(
  duration: Duration(milliseconds: 300),
  padding: EdgeInsets.all(isExpanded ? 32 : 8),
  child: Icon(Icons.star),
)

4. Gradient-Animation

AnimatedContainer(
  duration: Duration(milliseconds: 800),
  decoration: BoxDecoration(
    gradient: LinearGradient(
      colors: isDarkMode 
          ? [Colors.black, Colors.grey[900]!] 
          : [Colors.blue, Colors.purple],
    ),
  ),
)

5. Transform (Drehen/Skalieren)

AnimatedContainer(
  duration: Duration(milliseconds: 400),
  transform: Matrix4.rotationZ(isRotated ? 3.14 : 0),
  child: Icon(Icons.refresh),
)

Weitere implizite Animations-Widgets

Flutter bietet für unterschiedliche Anforderungen spezialisierte implizite Animations-Widgets:

AnimatedOpacity – Übergänge der Sichtbarkeit

Perfekt für „Fade-in“-Effekte wie auf Websites:

class FadeInDemo extends StatefulWidget {
  @override
  _FadeInDemoState createState() => _FadeInDemoState();
}

class _FadeInDemoState extends State<FadeInDemo> {
  bool _isVisible = false;

  @override
  void initState() {
    super.initState();
    // Show after 1 second
    Future.delayed(Duration(seconds: 1), () {
      setState(() => _isVisible = true);
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedOpacity(
      opacity: _isVisible ? 1.0 : 0.0,
      duration: Duration(milliseconds: 600),
      child: Text(
        'Hello World!',
        style: TextStyle(fontSize: 32),
      ),
    );
  }
}

Einsatzfälle:

  • Hinweismeldungen
  • Inhalte nach dem Laden einblenden
  • Hover-Effekte
  • Onboarding-Screens

AnimatedPositioned – Positionswechsel im Stack

class SlidingMenuDemo extends StatefulWidget {
  @override
  _SlidingMenuDemoState createState() => _SlidingMenuDemoState();
}

class _SlidingMenuDemoState extends State<SlidingMenuDemo> {
  bool _isMenuOpen = false;

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        // Main content
        Container(color: Colors.white),
        
        // Sliding menu
        AnimatedPositioned(
          duration: Duration(milliseconds: 300),
          curve: Curves.easeInOut,
          left: _isMenuOpen ? 0 : -250,
          top: 0,
          bottom: 0,
          width: 250,
          child: Container(
            color: Colors.blue,
            child: ListView(
              children: [
                ListTile(title: Text('Menu 1', style: TextStyle(color: Colors.white))),
                ListTile(title: Text('Menu 2', style: TextStyle(color: Colors.white))),
                ListTile(title: Text('Menu 3', style: TextStyle(color: Colors.white))),
              ],
            ),
          ),
        ),
        
        // Hamburger button
        Positioned(
          top: 50,
          left: 20,
          child: IconButton(
            icon: Icon(Icons.menu),
            onPressed: () {
              setState(() => _isMenuOpen = !_isMenuOpen);
            },
          ),
        ),
      ],
    );
  }
}

Einsatzfälle:

  • Drawer-Menüs
  • Position des Floating Action Button
  • Spielfiguren
  • Parallax-Effekte

AnimatedAlign – Übergänge der Ausrichtung

Widgets an eine andere Position innerhalb des Elternteils bewegen:

class BouncingBallDemo extends StatefulWidget {
  @override
  _BouncingBallDemoState createState() => _BouncingBallDemoState();
}

class _BouncingBallDemoState extends State<BouncingBallDemo> {
  Alignment _alignment = Alignment.topLeft;

  void _moveBall() {
    setState(() {
      _alignment = _alignment == Alignment.topLeft 
          ? Alignment.bottomRight 
          : Alignment.topLeft;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _moveBall,
      child: Container(
        width: 300,
        height: 300,
        color: Colors.grey[200],
        child: AnimatedAlign(
          alignment: _alignment,
          duration: Duration(seconds: 1),
          curve: Curves.bounceOut,
          child: Container(
            width: 50,
            height: 50,
            decoration: BoxDecoration(
              color: Colors.red,
              shape: BoxShape.circle,
            ),
          ),
        ),
      ),
    );
  }
}

AnimatedPadding – Animation der Abstände

AnimatedPadding(
  duration: Duration(milliseconds: 300),
  padding: EdgeInsets.all(isSelected ? 20 : 8),
  child: Card(
    child: Text('Selected Card'),
  ),
)

AnimatedDefaultTextStyle – Übergänge des Textstils

class TextStyleDemo extends StatefulWidget {
  @override
  _TextStyleDemoState createState() => _TextStyleDemoState();
}

class _TextStyleDemoState extends State<TextStyleDemo> {
  bool _isLarge = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isLarge = !_isLarge),
      child: AnimatedDefaultTextStyle(
        duration: Duration(milliseconds: 300),
        style: TextStyle(
          fontSize: _isLarge ? 48 : 24,
          color: _isLarge ? Colors.red : Colors.blue,
          fontWeight: _isLarge ? FontWeight.bold : FontWeight.normal,
        ),
        child: Text('Touch Me'),
      ),
    );
  }
}

AnimatedCrossFade – Übergang zwischen zwei Widgets

Wechselt geschmeidig zwischen zwei Widgets:

class ToggleViewDemo extends StatefulWidget {
  @override
  _ToggleViewDemoState createState() => _ToggleViewDemoState();
}

class _ToggleViewDemoState extends State<ToggleViewDemo> {
  bool _showFirst = true;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimatedCrossFade(
          firstChild: Container(
            width: 200,
            height: 200,
            color: Colors.blue,
            child: Center(child: Text('First Widget', style: TextStyle(color: Colors.white))),
          ),
          secondChild: Container(
            width: 200,
            height: 200,
            color: Colors.red,
            child: Center(child: Text('Second Widget', style: TextStyle(color: Colors.white))),
          ),
          crossFadeState: _showFirst 
              ? CrossFadeState.showFirst 
              : CrossFadeState.showSecond,
          duration: Duration(milliseconds: 500),
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _showFirst = !_showFirst),
          child: Text('Switch'),
        ),
      ],
    );
  }
}

Einsatzfälle:

  • Zwischen Listen- und Rasteransicht umschalten
  • Leeren Zustand ein- und ausblenden
  • Wechsel zwischen Login- und Registrierungsformular
  • Spielmodus wechseln

AnimatedSwitcher – beliebiger Widget-Wechsel

Flexibler als AnimatedCrossFade:

class CounterWithAnimation extends StatefulWidget {
  @override
  _CounterWithAnimationState createState() => _CounterWithAnimationState();
}

class _CounterWithAnimationState extends State<CounterWithAnimation> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedSwitcher(
          duration: Duration(milliseconds: 300),
          transitionBuilder: (child, animation) {
            return ScaleTransition(scale: animation, child: child);
          },
          child: Text(
            '$_count',
            key: ValueKey<int>(_count), // Important: Key required!
            style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
          ),
        ),
        SizedBox(height: 20),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              icon: Icon(Icons.remove),
              onPressed: () => setState(() => _count--),
            ),
            SizedBox(width: 20),
            IconButton(
              icon: Icon(Icons.add),
              onPressed: () => setState(() => _count++),
            ),
          ],
        ),
      ],
    );
  }
}

Profi-Tipp: Der Parameter key ist bei AnimatedSwitcher erforderlich. Flutter erkennt den Widget-Wechsel anhand des Keys.

Duration und Curve: die Seele der Animation

Duration

Legt fest, wie lange die Animation dauert:

// Very fast - Change without drawing attention
Duration(milliseconds: 150)

// Standard - Ideal for most uses
Duration(milliseconds: 300)

// Slow - To draw attention
Duration(milliseconds: 600)

// Very slow - Dramatic effects
Duration(seconds: 1)

Tipp zur Nutzererfahrung:

  • Kleine Änderungen: 150–200 ms
  • Mittlere Änderungen: 300–400 ms
  • Große Änderungen: 500–800 ms
  • Länger als eine Sekunde: lässt Nutzer warten

Curve

Das Beschleunigungs- und Abbremsverhalten der Animation:

// Linear - Constant speed (generally not used)
curve: Curves.linear

// Slow start, speed up, slow end - Most natural
curve: Curves.easeInOut

// Slow start
curve: Curves.easeIn

// Slow end
curve: Curves.easeOut

// Elastic - Bounce back feel
curve: Curves.elasticOut

// Bounce - Bouncing effect
curve: Curves.bounceOut

// Fast start, slow end
curve: Curves.decelerate

// Overshoot and return
curve: Curves.anticipate

Kurven im Vergleich

class CurveComparison extends StatefulWidget {
  @override
  _CurveComparisonState createState() => _CurveComparisonState();
}

class _CurveComparisonState extends State<CurveComparison> {
  bool _expanded = false;

  Widget _buildAnimatedBox(String label, Curve curve) {
    return Column(
      children: [
        Text(label, style: TextStyle(fontSize: 12)),
        AnimatedContainer(
          duration: Duration(seconds: 1),
          curve: curve,
          width: _expanded ? 200 : 50,
          height: 50,
          color: Colors.blue,
        ),
      ],
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        _buildAnimatedBox('linear', Curves.linear),
        SizedBox(height: 10),
        _buildAnimatedBox('easeInOut', Curves.easeInOut),
        SizedBox(height: 10),
        _buildAnimatedBox('bounceOut', Curves.bounceOut),
        SizedBox(height: 10),
        _buildAnimatedBox('elasticOut', Curves.elasticOut),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: () => setState(() => _expanded = !_expanded),
          child: Text('Start Animations'),
        ),
      ],
    );
  }
}

Einsatzfälle aus der Praxis

1. Ausklappbare Karte

class ExpandableCard extends StatefulWidget {
  final String title;
  final String content;

  ExpandableCard({required this.title, required this.content});

  @override
  _ExpandableCardState createState() => _ExpandableCardState();
}

class _ExpandableCardState extends State<ExpandableCard> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () => setState(() => _isExpanded = !_isExpanded),
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        curve: Curves.easeInOut,
        padding: EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [
            BoxShadow(
              color: Colors.black12,
              blurRadius: _isExpanded ? 10 : 5,
              spreadRadius: _isExpanded ? 2 : 0,
            ),
          ],
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  widget.title,
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
                ),
                AnimatedRotation(
                  duration: Duration(milliseconds: 300),
                  turns: _isExpanded ? 0.5 : 0,
                  child: Icon(Icons.expand_more),
                ),
              ],
            ),
            AnimatedCrossFade(
              firstChild: SizedBox.shrink(),
              secondChild: Padding(
                padding: EdgeInsets.only(top: 12),
                child: Text(widget.content),
              ),
              crossFadeState: _isExpanded 
                  ? CrossFadeState.showSecond 
                  : CrossFadeState.showFirst,
              duration: Duration(milliseconds: 300),
            ),
          ],
        ),
      ),
    );
  }
}

2. Animation eines Like-Buttons

class LikeButton extends StatefulWidget {
  @override
  _LikeButtonState createState() => _LikeButtonState();
}

class _LikeButtonState extends State<LikeButton> {
  bool _isLiked = false;
  int _likeCount = 42;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        setState(() {
          _isLiked = !_isLiked;
          _likeCount += _isLiked ? 1 : -1;
        });
      },
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          AnimatedContainer(
            duration: Duration(milliseconds: 200),
            curve: Curves.easeInOut,
            padding: EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: _isLiked ? Colors.red.withOpacity(0.2) : Colors.transparent,
              shape: BoxShape.circle,
            ),
            child: AnimatedSwitcher(
              duration: Duration(milliseconds: 200),
              transitionBuilder: (child, animation) {
                return ScaleTransition(scale: animation, child: child);
              },
              child: Icon(
                _isLiked ? Icons.favorite : Icons.favorite_border,
                key: ValueKey<bool>(_isLiked),
                color: _isLiked ? Colors.red : Colors.grey,
                size: 28,
              ),
            ),
          ),
          SizedBox(width: 4),
          AnimatedDefaultTextStyle(
            duration: Duration(milliseconds: 200),
            style: TextStyle(
              color: _isLiked ? Colors.red : Colors.grey,
              fontWeight: _isLiked ? FontWeight.bold : FontWeight.normal,
              fontSize: 16,
            ),
            child: Text('$_likeCount'),
          ),
        ],
      ),
    );
  }
}

3. Platzhalter beim Laden (Skeleton Screen)

class ShimmerLoading extends StatefulWidget {
  @override
  _ShimmerLoadingState createState() => _ShimmerLoadingState();
}

class _ShimmerLoadingState extends State<ShimmerLoading> {
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    // Show content after 3 seconds
    Future.delayed(Duration(seconds: 3), () {
      setState(() => _isLoading = false);
    });
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedCrossFade(
      firstChild: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            width: double.infinity,
            height: 200,
            decoration: BoxDecoration(
              color: Colors.grey[300],
              borderRadius: BorderRadius.circular(12),
            ),
          ),
          SizedBox(height: 12),
          Container(
            width: 200,
            height: 20,
            color: Colors.grey[300],
          ),
          SizedBox(height: 8),
          Container(
            width: 150,
            height: 20,
            color: Colors.grey[300],
          ),
        ],
      ),
      secondChild: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Image.network(
            'https://picsum.photos/400/200',
            height: 200,
            width: double.infinity,
            fit: BoxFit.cover,
          ),
          SizedBox(height: 12),
          Text('Title Goes Here', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
          SizedBox(height: 8),
          Text('Subtitle goes here', style: TextStyle(color: Colors.grey)),
        ],
      ),
      crossFadeState: _isLoading 
          ? CrossFadeState.showFirst 
          : CrossFadeState.showSecond,
      duration: Duration(milliseconds: 500),
    );
  }
}

4. Floating Action Button verwandeln

class MorphingFAB extends StatefulWidget {
  @override
  _MorphingFABState createState() => _MorphingFABState();
}

class _MorphingFABState extends State<MorphingFAB> {
  bool _isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: Duration(milliseconds: 300),
      curve: Curves.easeInOut,
      width: _isExpanded ? 200 : 56,
      height: 56,
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(28),
      ),
      child: Material(
        color: Colors.transparent,
        child: InkWell(
          borderRadius: BorderRadius.circular(28),
          onTap: () => setState(() => _isExpanded = !_isExpanded),
          child: Padding(
            padding: EdgeInsets.symmetric(horizontal: 16),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.add, color: Colors.white),
                if (_isExpanded) ...[
                  SizedBox(width: 12),
                  Text(
                    'Add New',
                    style: TextStyle(color: Colors.white, fontSize: 16),
                  ),
                ],
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Tipps für die Performance

1. Verwenden Sie const-Konstruktoren

// ❌ Bad - New instance every rebuild
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  child: Text('Static Text'),
)

// ✅ Good - Const child is cached
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  child: const Text('Static Text'),
)

2. Nutzen Sie AnimatedWidget nicht ohne Grund

// ❌ Bad - Container is enough if only color changes
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  width: 100, // Never changes
  height: 100, // Never changes
  color: selectedColor,
)

// ✅ Good - Only animate what's needed
Container(
  width: 100,
  height: 100,
  child: AnimatedContainer(
    duration: Duration(milliseconds: 300),
    color: selectedColor,
  ),
)

3. Optimieren Sie mit RepaintBoundary

RepaintBoundary(
  child: AnimatedOpacity(
    opacity: isVisible ? 1.0 : 0.0,
    duration: Duration(milliseconds: 300),
    child: HeavyWidget(),
  ),
)

4. Animieren Sie nicht zu vieles gleichzeitig

// ❌ Bad - 100 widgets animating at once
ListView.builder(
  itemCount: 100,
  itemBuilder: (context, index) {
    return AnimatedContainer(
      duration: Duration(seconds: 1),
      height: heights[index],
      child: ListTile(title: Text('Item $index')),
    );
  },
)

// ✅ Good - Only visible ones should animate

Häufige Fehler und ihre Lösungen

Fehler 1: Key beim AnimatedSwitcher vergessen

// ❌ Wrong - Animation won't work
AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: Text('$count'), // No key!
)

// ✅ Correct
AnimatedSwitcher(
  duration: Duration(milliseconds: 300),
  child: Text('$count', key: ValueKey(count)),
)

Fehler 2: zu lange Dauer

// ❌ User waits 3 seconds
AnimatedContainer(
  duration: Duration(seconds: 3),
  color: newColor,
)

// ✅ Fast and responsive
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  color: newColor,
)

Fehler 3: falsche Wahl der Kurve

// ❌ Linear looks too mechanical
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  curve: Curves.linear,
  width: newWidth,
)

// ✅ Natural looking
AnimatedContainer(
  duration: Duration(milliseconds: 300),
  curve: Curves.easeInOut,
  width: newWidth,
)

Alle impliziten Animations-Widgets

Flutter bietet über 15 implizite Animations-Widgets:

Widget Was es animiert
AnimatedContainer Alle Eigenschaften des Container
AnimatedOpacity Deckkraft (Transparenz)
AnimatedPadding Padding (Abstände)
AnimatedPositioned Position im Stack
AnimatedAlign Ausrichtung
AnimatedDefaultTextStyle Textstil
AnimatedPhysicalModel Schatten und Elevation
AnimatedCrossFade Übergang zwischen zwei Widgets
AnimatedSwitcher Beliebiger Widget-Wechsel
AnimatedSize Größe des Widgets
AnimatedRotation Drehwinkel
AnimatedScale Skalierung
AnimatedSlide Verschiebung (Offset)
AnimatedFractionallySizedBox Größe relativ zum Elternteil
AnimatedTheme Theme-Änderungen

Zusammenfassung

  • Implizite Animationen: Animation ohne Zusatzcode (setState genügt)
  • AnimatedContainer: Das vielseitigste implizite Animations-Widget
  • Duration: Dauer der Animation (ideal: 200–400 ms)
  • Curve: Beschleunigungs- und Abbremsverhalten (ideal: easeInOut)
  • AnimatedOpacity: Für Ein- und Ausblendeffekte
  • AnimatedPositioned: Positionswechsel im Stack
  • AnimatedCrossFade: Übergang zwischen zwei Widgets
  • AnimatedSwitcher: Beliebiger Widget-Wechsel (Key erforderlich!)
  • Performance: const, RepaintBoundary, keine unnötigen Animationen
  • UX: Kurze Dauer, natürliche Kurven, gezielter Einsatz

Implizite Animationen gehören zu den stärksten Funktionen von Flutter. Sie erzielen mit minimalem Code maximale Wirkung. Die Kombination aus setState und einem Animated*-Widget ist alles, was Sie brauchen, um Ihre App lebendig zu machen!

Kommentare