From 7fa1345633c06e6a37537fc3c30726a8b8ad2d8d Mon Sep 17 00:00:00 2001 From: Alysson Date: Sun, 7 May 2023 14:39:42 +0200 Subject: [PATCH] feat(): riverpod & state management --- lib/main.dart | 3 +- lib/providers/favorites_provider.dart | 20 +++ lib/providers/filters_provider.dart | 55 +++++++ lib/providers/meals_provider.dart | 4 + lib/screens/CategoriesScreen.dart | 10 +- lib/screens/MealsScreen.dart | 12 +- lib/screens/filters.dart | 198 ++++++++++---------------- lib/screens/meal_details.dart | 28 ++-- lib/screens/tabs.dart | 70 ++------- pubspec.lock | 24 ++++ pubspec.yaml | 1 + 11 files changed, 224 insertions(+), 201 deletions(-) create mode 100644 lib/providers/favorites_provider.dart create mode 100644 lib/providers/filters_provider.dart create mode 100644 lib/providers/meals_provider.dart diff --git a/lib/main.dart b/lib/main.dart index 630fa44..e86f8d9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:meals/screens/CategoriesScreen.dart'; import 'package:meals/screens/tabs.dart'; @@ -13,7 +14,7 @@ final theme = ThemeData( ); void main() { - runApp(const App()); + runApp(const ProviderScope(child: App())); } class App extends StatelessWidget { diff --git a/lib/providers/favorites_provider.dart b/lib/providers/favorites_provider.dart new file mode 100644 index 0000000..5c4adcc --- /dev/null +++ b/lib/providers/favorites_provider.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meals/models/Meal.dart'; + +class FavoriteMealsNotifier extends StateNotifier> { + FavoriteMealsNotifier(List? initialList) : super(initialList ?? []); + + bool toggleMealFavoriteStatus(Meal meal) { + if (state.contains(meal)) { + state = state.where((Meal m) => m.id != meal.id).toList(); + return false; + } + + state = [...state, meal]; + return true; + } +} + +final favoriteMealsProvider = +StateNotifierProvider>( + (ref) => FavoriteMealsNotifier([])); diff --git a/lib/providers/filters_provider.dart b/lib/providers/filters_provider.dart new file mode 100644 index 0000000..9553a1a --- /dev/null +++ b/lib/providers/filters_provider.dart @@ -0,0 +1,55 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meals/providers/meals_provider.dart'; +import 'package:meals/screens/filters.dart'; + +enum Filter { + glutenFree, + lactoseFree, + veggie, + vegan, +} + +class FiltersNotifier extends StateNotifier> { + FiltersNotifier() + : super({ + Filter.glutenFree: false, + Filter.lactoseFree: false, + Filter.veggie: false, + Filter.vegan: false, + }); + + void setFilter(Filter filter, bool isActive) { + state = { + ...state, + filter: isActive, + }; + } + + void setFilters(Map newFilters) { + state = newFilters; + } +} + +final filtersProvider = + StateNotifierProvider>( + (ref) => FiltersNotifier()); + +final filteredMealsProvider = Provider((ref) { + final meals = ref.watch(mealsProvider); + final activeFilters = ref.watch(filtersProvider); + return meals.where((meal) { + if (activeFilters[Filter.glutenFree]! && !meal.isGlutenFree) { + return false; + } + if (activeFilters[Filter.lactoseFree]! && !meal.isLactoseFree) { + return false; + } + if (activeFilters[Filter.veggie]! && !meal.isVegetarian) { + return false; + } + if (activeFilters[Filter.vegan]! && !meal.isVegan) { + return false; + } + return true; + }).toList(); +}); diff --git a/lib/providers/meals_provider.dart b/lib/providers/meals_provider.dart new file mode 100644 index 0000000..cd4936e --- /dev/null +++ b/lib/providers/meals_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meals/data/dummy_categories.dart'; + +final mealsProvider = Provider((ref) => dummyMeals); diff --git a/lib/screens/CategoriesScreen.dart b/lib/screens/CategoriesScreen.dart index 0cb25ce..57d4cf9 100644 --- a/lib/screens/CategoriesScreen.dart +++ b/lib/screens/CategoriesScreen.dart @@ -7,11 +7,11 @@ import 'package:meals/widgets/CategoryGridItem.dart'; class CategoriesScreen extends StatelessWidget { final List availableMeals; - final void Function(Meal meal) onToggleFavorite; - const CategoriesScreen({super.key, + const CategoriesScreen({ + super.key, required this.availableMeals, - required this.onToggleFavorite}); + }); void _selectCategory(BuildContext context, Category category) { final meals = availableMeals @@ -21,11 +21,9 @@ class CategoriesScreen extends StatelessWidget { Navigator.push( context, MaterialPageRoute( - builder: (ctx) => - MealsScreen( + builder: (ctx) => MealsScreen( title: category.title, meals: meals, - onToggleFavorite: onToggleFavorite, ))); } diff --git a/lib/screens/MealsScreen.dart b/lib/screens/MealsScreen.dart index 4cbf0c0..755be73 100644 --- a/lib/screens/MealsScreen.dart +++ b/lib/screens/MealsScreen.dart @@ -6,13 +6,12 @@ import 'package:meals/widgets/MealItem.dart'; class MealsScreen extends StatelessWidget { final String? title; final List meals; - final void Function(Meal meal) onToggleFavorite; - const MealsScreen( - {super.key, - this.title, - required this.meals, - required this.onToggleFavorite}); + const MealsScreen({ + super.key, + this.title, + required this.meals, + }); void _selectMeal(BuildContext buildContext, Meal meal) { Navigator.push( @@ -20,7 +19,6 @@ class MealsScreen extends StatelessWidget { MaterialPageRoute( builder: (ctx) => MealDetailScreen( meal: meal, - onToggleFavorite: onToggleFavorite, ), )); } diff --git a/lib/screens/filters.dart b/lib/screens/filters.dart index 83628c1..c9d3b87 100644 --- a/lib/screens/filters.dart +++ b/lib/screens/filters.dart @@ -1,134 +1,94 @@ -import 'package:flutter/material.dart'; -import 'package:meals/screens/tabs.dart'; -import 'package:meals/widgets/main_drawer.dart'; - -enum Filter { - glutenFree, - lactoseFree, - veggie, - vegan, -} - -class FiltersScreen extends StatefulWidget { - final Map currentFilters; +import 'dart:developer'; - const FiltersScreen({Key? key, required this.currentFilters}) - : super(key: key); - - @override - State createState() => _FiltersScreenState(); -} +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class _FiltersScreenState extends State { - bool _glutenFreeFilterSet = false; - bool _lactoseFreeFilterSet = false; - bool _veggieFilterSet = false; - bool _veganFilterSet = false; +import '../providers/filters_provider.dart'; - @override - void initState() { - super.initState(); - setState(() { - _glutenFreeFilterSet = widget.currentFilters[Filter.glutenFree]!; - _lactoseFreeFilterSet = widget.currentFilters[Filter.lactoseFree]!; - _veggieFilterSet = widget.currentFilters[Filter.veggie]!; - _veganFilterSet = widget.currentFilters[Filter.vegan]!; - }); - } +class FiltersScreen extends ConsumerWidget { + const FiltersScreen({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( title: const Text('Your filters'), ), - body: WillPopScope( - onWillPop: () async { - Navigator.of(context).pop({ - Filter.glutenFree: _glutenFreeFilterSet, - Filter.lactoseFree: _lactoseFreeFilterSet, - Filter.veggie: _veggieFilterSet, - Filter.vegan: _veganFilterSet, - }); - - return false; - }, - child: Column( - children: [ - SwitchListTile( - value: _glutenFreeFilterSet, - onChanged: (checked) => setState(() { - _glutenFreeFilterSet = checked; - }), - title: Text( - 'Gluten free', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - subtitle: Text( - 'Only includes gluten-free meals', - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - activeColor: Theme.of(context).colorScheme.tertiary, - contentPadding: const EdgeInsets.only(left: 34, right: 22), + body: Column( + children: [ + SwitchListTile( + value: ref.watch(filtersProvider)[Filter.glutenFree]!, + onChanged: (checked) => ref + .read(filtersProvider.notifier) + .setFilter(Filter.glutenFree, checked), + title: Text( + 'Gluten free', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + ), + subtitle: Text( + 'Only includes gluten-free meals', + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + ), + activeColor: Theme.of(context).colorScheme.tertiary, + contentPadding: const EdgeInsets.only(left: 34, right: 22), + ), + SwitchListTile( + value: ref.watch(filtersProvider)[Filter.lactoseFree]!, + onChanged: (checked) => ref + .read(filtersProvider.notifier) + .setFilter(Filter.lactoseFree, checked), + title: Text( + 'Lactose free', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + ), + subtitle: Text( + 'Only includes lactose-free meals', + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + ), + activeColor: Theme.of(context).colorScheme.tertiary, + contentPadding: const EdgeInsets.only(left: 34, right: 22), + ), + SwitchListTile( + value: ref.watch(filtersProvider)[Filter.veggie]!, + onChanged: (checked) => ref + .read(filtersProvider.notifier) + .setFilter(Filter.veggie, checked), + title: Text( + 'Veggie', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground), + ), + subtitle: Text( + 'Only includes veggie meals', + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground), ), - SwitchListTile( - value: _lactoseFreeFilterSet, - onChanged: (checked) => setState(() { - _lactoseFreeFilterSet = checked; - }), - title: Text( - 'Lactose free', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - subtitle: Text( - 'Only includes lactose-free meals', - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - activeColor: Theme.of(context).colorScheme.tertiary, - contentPadding: const EdgeInsets.only(left: 34, right: 22), + activeColor: Theme.of(context).colorScheme.tertiary, + contentPadding: const EdgeInsets.only(left: 34, right: 22), + ), + SwitchListTile( + value: ref.watch(filtersProvider)[Filter.vegan]!, + onChanged: (checked) => ref + .read(filtersProvider.notifier) + .setFilter(Filter.vegan, checked), + title: Text( + 'Vegan', + style: Theme.of(context).textTheme.titleLarge!.copyWith( + color: Theme.of(context).colorScheme.onBackground), ), - SwitchListTile( - value: _veggieFilterSet, - onChanged: (checked) => setState(() { - _veggieFilterSet = checked; - }), - title: Text( - 'Veggie', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - subtitle: Text( - 'Only includes veggie meals', - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - activeColor: Theme.of(context).colorScheme.tertiary, - contentPadding: const EdgeInsets.only(left: 34, right: 22), + subtitle: Text( + 'Only includes vegan meals', + style: Theme.of(context).textTheme.labelMedium!.copyWith( + color: Theme.of(context).colorScheme.onBackground), ), - SwitchListTile( - value: _veganFilterSet, - onChanged: (checked) => setState(() { - _veganFilterSet = checked; - }), - title: Text( - 'Vegan', - style: Theme.of(context).textTheme.titleLarge!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - subtitle: Text( - 'Only includes vegan meals', - style: Theme.of(context).textTheme.labelMedium!.copyWith( - color: Theme.of(context).colorScheme.onBackground), - ), - activeColor: Theme.of(context).colorScheme.tertiary, - contentPadding: const EdgeInsets.only(left: 34, right: 22), - ) - ], - ), + activeColor: Theme.of(context).colorScheme.tertiary, + contentPadding: const EdgeInsets.only(left: 34, right: 22), + ) + ], )); } } diff --git a/lib/screens/meal_details.dart b/lib/screens/meal_details.dart index 419902f..05a91be 100644 --- a/lib/screens/meal_details.dart +++ b/lib/screens/meal_details.dart @@ -1,24 +1,36 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meals/providers/favorites_provider.dart'; import '../models/Meal.dart'; -class MealDetailScreen extends StatelessWidget { +class MealDetailScreen extends ConsumerWidget { final Meal meal; - final void Function(Meal meal) onToggleFavorite; - const MealDetailScreen( - {Key? key, required this.meal, required this.onToggleFavorite}) - : super(key: key); + const MealDetailScreen({Key? key, required this.meal}) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final favoriteMeals = ref.watch(favoriteMealsProvider); + + final isIntoFavorite = favoriteMeals.contains(meal); + return Scaffold( appBar: AppBar( title: Text(meal.title), actions: [ IconButton( - onPressed: () => onToggleFavorite(meal), - icon: const Icon(Icons.star)) + onPressed: () { + final wasAdded = ref + .read(favoriteMealsProvider.notifier) + .toggleMealFavoriteStatus(meal); + ScaffoldMessenger.of(context).clearSnackBars(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(wasAdded + ? 'Meal has been added to your favorite.' + : 'Meal is no longer as a favorite.'))); + }, + icon: Icon(isIntoFavorite ? Icons.star : Icons.star_border)) ], ), body: SingleChildScrollView( diff --git a/lib/screens/tabs.dart b/lib/screens/tabs.dart index b43008b..fba47ee 100644 --- a/lib/screens/tabs.dart +++ b/lib/screens/tabs.dart @@ -1,36 +1,22 @@ import 'package:flutter/material.dart'; -import 'package:meals/data/dummy_categories.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:meals/providers/favorites_provider.dart'; import 'package:meals/screens/CategoriesScreen.dart'; import 'package:meals/screens/MealsScreen.dart'; import 'package:meals/screens/filters.dart'; import 'package:meals/widgets/main_drawer.dart'; -import '../models/Meal.dart'; +import '../providers/filters_provider.dart'; -const kDefaultFilters = { - Filter.glutenFree: false, - Filter.lactoseFree: false, - Filter.veggie: false, - Filter.vegan: false, -}; - -class TabsScreen extends StatefulWidget { +class TabsScreen extends ConsumerStatefulWidget { const TabsScreen({Key? key}) : super(key: key); @override - State createState() => _TabsScreenState(); + ConsumerState createState() => _TabsScreenState(); } -class _TabsScreenState extends State { +class _TabsScreenState extends ConsumerState { int _selectedPageIndex = 0; - final List _favoriteMeals = []; - Map _selectedFilters = kDefaultFilters; - - void _showInfoMessage(String message) { - ScaffoldMessenger.of(context).clearSnackBars(); - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(message))); - } void _selectPage(int index) { setState(() { @@ -38,33 +24,13 @@ class _TabsScreenState extends State { }); } - void _toggleFavoriteMeal(Meal meal) { - if (_favoriteMeals.contains(meal)) { - setState(() { - _favoriteMeals.remove(meal); - }); - _showInfoMessage('Meal is no longer a favorite'); - } else { - setState(() { - _favoriteMeals.add(meal); - _showInfoMessage('Meal has been added to your favorites'); - }); - } - } - void _setScreen(String identifier) async { Navigator.of(context).pop(); switch (identifier) { case 'filters': - final results = await Navigator.of(context) - .push>(MaterialPageRoute( - builder: (ctx) => FiltersScreen( - currentFilters: _selectedFilters, - ))); - setState(() { - _selectedFilters = results ?? kDefaultFilters; - }); + await Navigator.of(context).push>( + MaterialPageRoute(builder: (ctx) => const FiltersScreen())); break; } } @@ -73,35 +39,19 @@ class _TabsScreenState extends State { Widget build(BuildContext context) { Widget activePage; String? activePageTitle; - final availableMeals = dummyMeals.where((meal) { - if (_selectedFilters[Filter.glutenFree]! && !meal.isGlutenFree) { - return false; - } - if (_selectedFilters[Filter.lactoseFree]! && !meal.isLactoseFree) { - return false; - } - if (_selectedFilters[Filter.veggie]! && !meal.isVegetarian) { - return false; - } - if (_selectedFilters[Filter.vegan]! && !meal.isVegan) { - return false; - } - return true; - }).toList(); + final availableMeals = ref.watch(filteredMealsProvider); switch (_selectedPageIndex) { case 1: activePageTitle = 'Favorites'; activePage = MealsScreen( - meals: _favoriteMeals, - onToggleFavorite: _toggleFavoriteMeal, + meals: ref.watch(favoriteMealsProvider), ); break; default: activePageTitle = 'Pick your category'; activePage = CategoriesScreen( availableMeals: availableMeals, - onToggleFavorite: _toggleFavoriteMeal, ); } diff --git a/pubspec.lock b/pubspec.lock index e15321e..624ea75 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -94,6 +94,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + url: "https://pub.dev" + source: hosted + version: "2.3.6" flutter_test: dependency: "direct dev" description: flutter @@ -243,6 +251,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.4" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + url: "https://pub.dev" + source: hosted + version: "2.3.6" sky_engine: dependency: transitive description: flutter @@ -264,6 +280,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" + source: hosted + version: "0.7.2+1" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 30e9b07..81fa708 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,6 +37,7 @@ dependencies: cupertino_icons: ^1.0.2 google_fonts: ^4.0.3 transparent_image: ^2.0.1 + flutter_riverpod: ^2.3.6 dev_dependencies: flutter_test: -- 2.39.5