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