Search code examples
flutterdartflutter-animationflutter-state

Custom Widget revising animation in gridview


I have a custom widget that changes color when tapped inside a gridview. When I scroll to the bottom and scroll back up to the top selected widget its animation is reversed.

I'm pretty sure that it has something to do with the widget being disposed of when out of view but I don't have a solution to overcome it. See my code below:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Thirty Seconds',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

// Page with the gridview
class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GridView.count(
        crossAxisCount: 2,
        children: List.generate(20, (index) {
          return MyCustomWidget(
            key: GlobalKey(),
            index: index + 1,
          );
        }),
      ),
    );
  }
}

// Custom Widget
class MyCustomWidget extends StatefulWidget {
  const MyCustomWidget({
    super.key,
    required this.index,
  });

  final int index;

  @override
  State<MyCustomWidget> createState() => _MyCustomWidgetState();
}

class _MyCustomWidgetState extends State<MyCustomWidget>
    with SingleTickerProviderStateMixin {
  late AnimationController _animationController;
  late Animation<Color?> _colorAnimation;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _colorAnimation = ColorTween(begin: Colors.white, end: Colors.yellow)
        .animate(_animationController)
      ..addListener(() => setState(() {}));
  }

  @override
  void dispose() {
    _animationController.dispose();
    super.dispose();
  }

  void _toggleAnimation() {
    if (_animationController.isCompleted) {
      _animationController.reverse();
    } else {
      _animationController.forward();
    }
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        _toggleAnimation();
      },
      child: Container(
        color: _colorAnimation.value,
        child: Center(
          child: Text("Custom Widget ${widget.index}"),
        ),
      ),
    );
  }
}

Solution

  • GridView dispose the widget that aren't visible on UI. You can use cacheExtent(not suitable for this case) or AutomaticKeepAliveClientMixin on _MyCustomWidgetState.

    class _MyCustomWidgetState extends State<MyCustomWidget>
        with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
    
      @override
      bool get wantKeepAlive => true;
    
      @override
      Widget build(BuildContext context) {
        super.build(context);
    

    You may prefer handing it parent widget and passing a bool to check active state or state-management or project level depends on scenario.