Explicit Animation In Flutter

Explicit Animation In Flutter

Previous stateless widget:

import 'package:flutter/material.dart';
import 'package:meals/data/dummy_data.dart';
import 'package:meals/models/category.dart';
import 'package:meals/models/meal.dart';
import 'package:meals/screens/meals.dart';
import 'package:meals/widgets/category_grid_item.dart';

class CategoriesScreen extends StatelessWidget {
  const CategoriesScreen(
      {super.key,
      required this.onToggleFavorite,
      required this.availableMeals});

  final void Function(Meal meal) onToggleFavorite;

  final List<Meal> availableMeals;

  void _selectCategory(BuildContext context, Category category) {
    final filteredMeals = availableMeals
        .where(
          (meal) => meal.categories.contains(category.id),
        )
        .toList();
    // alternative=  Navigator.push(context,route)
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => MealsScreen(
          title: category.title,
          meals: filteredMeals,
          onToggleFavorite: onToggleFavorite,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GridView(
      padding: const EdgeInsets.all(24),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 3 / 2,
        crossAxisSpacing: 20,
        mainAxisSpacing: 20,
      ),
      children: [
        // alternavtive for ...availableCategories.map((category)=>CategoryGridItem(category: category)).toList()
        for (final category in availableCategories)
          CategoryGridItem(
              category: category,
              onSelectCategory: () {
                _selectCategory(context, category);
              })
      ],
    );
  }
}

Now to add animation, we must transform it to stateful widget which I assume you are familiar with:

import 'package:flutter/material.dart';
import 'package:meals/data/dummy_data.dart';
import 'package:meals/models/category.dart';
import 'package:meals/models/meal.dart';
import 'package:meals/screens/meals.dart';
import 'package:meals/widgets/category_grid_item.dart';

class CategoriesScreen extends StatefulWidget {
  const CategoriesScreen({
    super.key,
    required this.onToggleFavorite,
    required this.availableMeals,
  });

  final void Function(Meal meal) onToggleFavorite;
  final List<Meal> availableMeals;

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

class _CategoriesScreenState extends State<CategoriesScreen> {
  void _selectCategory(BuildContext context, Category category) {
    final filteredMeals = widget.availableMeals
        .where(
          (meal) => meal.categories.contains(category.id),
        )
        .toList();

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => MealsScreen(
          title: category.title,
          meals: filteredMeals,
          onToggleFavorite: widget.onToggleFavorite,
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return GridView(
      padding: const EdgeInsets.all(24),
      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
        crossAxisCount: 2,
        childAspectRatio: 3 / 2,
        crossAxisSpacing: 20,
        mainAxisSpacing: 20,
      ),
      children: [
        for (final category in availableCategories)
          CategoryGridItem(
            category: category,
            onSelectCategory: () {
              _selectCategory(context, category);
            },
          ),
      ],
    );
  }
}

now we will :

  • create a variable of AnimationController as late

  • add SingleTickerProviderStateMixin with class

  • declare the value of animationController which are vsync,duration,lowerBound,upperBound and to make sure it does only start after UI is render add forward function to it, here is the code below:

class _CategoriesScreenState extends State<CategoriesScreen>
    with SingleTickerProviderStateMixin {
  // TickerProviderStateMixing ; for multiple animation

  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this, // this -> set entire class here
      duration: const Duration(milliseconds: 600),
      lowerBound: 0, // default value
      upperBound: 1, // default value
    );
    _animationController.forward();
  }

and we must add dispose to it

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

now we will update the Widget build return method so that our class can handle animation so that it can work smoothly, for that we will update using AnimateBuilder, here is a simple introduction of AnimateBuilder:

AnimatedBuilder is a widget that makes it easy to create smooth animations by connecting an animation object to the UI. It requires an animation, usually managed by an AnimationController, to control the timing and progress of the animation. The builder function in AnimatedBuilder runs every time the animation updates, allowing you to dynamically rebuild parts of your UI based on the current animation value. Additionally, AnimatedBuilder has a child parameter that can hold static parts of the widget to avoid rebuilding them, which helps improve performance by focusing only on the animated elements.

here is the compete code below:

import 'package:flutter/material.dart';

import 'package:meals_app_animation/data/dummy_data.dart';
import 'package:meals_app_animation/models/meal.dart';
import 'package:meals_app_animation/widgets/category_grid_item.dart';
import 'package:meals_app_animation/screens/meals.dart';
import 'package:meals_app_animation/models/category.dart';

class CategoriesScreen extends StatefulWidget {
  const CategoriesScreen({
    super.key,
    required this.availableMeals,
  });

  final List<Meal> availableMeals;

  @override
  State<CategoriesScreen> createState() => _CategoriesScreenState();
}

class _CategoriesScreenState extends State<CategoriesScreen>
    with SingleTickerProviderStateMixin {
  // TickerProviderStateMixing ; for multiple animation

  late AnimationController _animationController;

  @override
  void initState() {
    super.initState();
    _animationController = AnimationController(
      vsync: this, // this -> set entire class here
      duration: const Duration(milliseconds: 600),
      lowerBound: 0, // default value
      upperBound: 1, // default value
    );
    _animationController.forward();
  }

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

  void _selectCategory(BuildContext context, Category category) {
    final filteredMeals = widget.availableMeals
        .where((meal) => meal.categories.contains(category.id))
        .toList();

    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (ctx) => MealsScreen(
          title: category.title,
          meals: filteredMeals,
        ),
      ),
    ); // Navigator.push(context, route)
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animationController,
      child: GridView(
        padding: const EdgeInsets.all(24),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 3 / 2,
          crossAxisSpacing: 20,
          mainAxisSpacing: 20,
        ),
        children: [
          // availableCategories.map((category) => CategoryGridItem(category: category)).toList()
          for (final category in availableCategories)
            CategoryGridItem(
              category: category,
              onSelectCategory: () {
                _selectCategory(context, category);
              },
            )
        ],
      ),
      builder: (context, child) => SlideTransition(
        position: Tween(
          begin: const Offset(0, 0.3),
          end: const Offset(0, 0),
        ).animate(
          CurvedAnimation(
              parent: _animationController, curve: Curves.easeInOut),
        ), // tween an object, 0 =0%, 1=100%

        child: child,
      ),
      // builder: (context, child) => Padding(
      //     padding: EdgeInsets.only(
      //       top: 100 - _animationController.value * 100,
      //     ),
      //     child: child),
    ); // child value seprare so that only Padding is done 60hz not entire grid
  }
}

I hope you have a better understanding of explicit animation builder