This is part two of the series in which you will build a food recipe app from scratch. If you haven’t read the first portion, I would suggest going through it before you read this part: https://www.topcoder.com/thrive/articles/building-a-food-recipe-app-from-scratch-with-firebase-and-spoonacular-api
In this article, I will explain state management using StateNotifier and freezed.
In this article you will learn:
What is state management?
Importance of following a state management technique
State management in Flutter
Usage of StateNotifier + freezed for state management
Application of the above to fav page
State management refers to the management of the state of one or more user interface controls such as buttons, text displays, list updation, and so on.
In a client application, states of UI are dependent on other UI elements. For example, when you are using an application that requires you to enter your name to proceed further, if you don’t enter your name and try to proceed to the next page, the next button has to be disabled or some error message has to be displayed. Or, during web page loading, a circular progress bar is displayed before the state of the application is changed to the fetched data from API or some other data source.
As more features are added, management of the states becomes complex. Therefore, following good state management is crucial in application development.
It helps in centralizing various states of UI which helps to easily handle data across the application
For example, when you want to display information related to a user on various screens of the app, requiring each page to handle the current state of the user makes the code redundant, thereby increasing the code complexity and making it hard to scale in the future.
There are many types of state management in Flutter. You can go through them here: https://flutter.dev/docs/development/data-and-backend/state-mgmt/options
The provider is a wrapper around InheritedWidget which makes it easy to use and improves reusability.
By using the provider you get the advantages of simplified allocation/disposal of resources, lazy-loading, reduced boilerplate, increased scalability for classes.
You can read more about providers here.
We install the dependencies.
1# pubspec.yaml
2dependencies:
3 freezed_annotation:
4 flutter_state_notifier: ^ 0.7 .0
5dev_dependencies:
6 build_runner:
7 freezed:
8We create the states.
What are the basic states you can think of when you load an app?
State for loading
State after loading
Error state
For example :
1abstract class User {}
2class Loading extends User {}
3class Success extends User {
4 final String username;
5 final String age;
6 Success({
7 required this.username,
8 required this.age
9 });
10}
11class Error extends User {
12 final Object error;
13 Error({
14 required this.error
15 });
16}
17We create our StateNotifier class:
1class UserController extends StateNotifier < User > {
2 UserController({
3 required MatchingState currentState,
4 }): super(Loading()) {
5 initialize();
6 }
7
8 void initialize() {
9
10 try {
11 //fetch some data
12 //update state to success when fetching is completed
13 state = Success(username: response, age: response);
14
15 } catch (e) {
16 //update status to error if some error is encountered
17 state = Error(error: e);
18
19 }
20 }
21}
22We consume our StateNotifier in the UI:
1final state = context.watch < User > ();
2if (state is Loading)
3 //show progress indicator
4 if (state is Success)
5 //display user details
6 if (state is Error)
7//show error message
8Instead of having several abstract classes, wouldn’t it be much easier to have the states simplified? Having many abstract classes increases the boilerplate code and gets hard to manage with several states.
This is where freezed package comes in handy - a great package that works amazingly with StateNotifer.
1import 'package:freezed_annotation/freezed_annotation.dart';
2
3part 'user.freezed.dart';
4
5@freezed
6abstract class User with _$User {
7 const factory User.loading() = Loading;
8 const factory User.error() = Error;
9 const factory User.notLogged() = UserNotLogged
10
11}
12As you can see, all the states are simplified as sealed classes instead of abstract classes.
In our app, we have a page to display the favorite list of recipes. Let’s break down the states of our favorite page:
Overall the page has these states:
Loading state when we fetch the data
Error state when fetching is interrupted
Success state when fetching is completed
For this article, I will be using dummy data and using future methods for understanding. Later we will be using Firebase to fetch data.
First, create a directory called data following clean architecture.
Add the class :
1class RecipeDetail {
2 final String image;
3
4 RecipeDetail({
5 required this.image,
6 });
7}
8Repository to fetch data:
9 final list = [
10 RecipeDetail(image: 'images/pasta.jpeg'),
11 RecipeDetail(image: 'images/cream.jpeg'),
12 RecipeDetail(image: 'images/food3.jpeg'),
13];
14class FavListRepository {
15 Future < List < RecipeDetail >> favList() async {
16 return await Future.delayed(
17 Duration(milliseconds: 500),
18 () => list,
19 );
20 }
21}
22We will return the list from the fav list function. A delay is added to show how the UI changes state during the load state.
The pressed fav recipes have to be displayed on the fav list page.
Let’s create the freezed class:
1import 'package:freezed_annotation/freezed_annotation.dart';
2
3part 'fav_list_view_model.freezed.dart';
4
5@freezed
6class FavListViewModel with _$FavListViewModel {
7 factory FavListViewModel({
8 @Default([]) List < RecipeDetail > favList,
9 }) = _FavListViewModel;
10
11 const factory FavListViewModel.loading() = Loading;
12
13 const factory FavListViewModel.error() = Error;
14}
15For displaying the list of favs we have three states:
Loading — when data is being loaded using a future.
Success- when fetching is completed
Let’s create a class of FavoriteItem
Error- when error has occurred during fetching
Let’s create the stateNotifier class:
1class FavPageListController extends StateNotifier < FavListViewModel > {
2 FavPageListController(): super(FavListViewModel.loading()) {
3 initialize();
4 }
5
6 Future < void > initialize() async {
7 try {
8 final favList = await FavListRepository()
9 .favList();
10 state = FavListViewModel(favList: favList);
11 } catch (e) {
12 state = FavListViewModel.error();
13 }
14 }
15}
16The initial State will be loading.
After fetching is completed we will update the state to ListItem with the fetched data.
Lets add this to the UI
1class FavListPage extends StatelessWidget {
2 FavListPage({
3 Key ? key
4 }): super(key: key);
5 late PageController pageController =
6 PageController(initialPage: 0, viewportFraction: 0.9);
7
8 @override
9 Widget build(BuildContext context) {
10 // 1
11 return
12 StateNotifierProvider < FavPageListController, FavListViewModel > (
13 create: (_) => FavPageListController(),
14 child: Scaffold(
15 appBar: AppBar(
16 title: Text(
17 'Favorite List',
18 ),
19 ),
20 body: Builder(
21 builder: (context) {
22 return context.watch < FavListViewModel > ()
23 .when(
24 (favList) {
25 //2
26 if (favList.isEmpty) {
27 return Center(
28 child: Text('Empty List'),
29 );
30 }
31 //3
32 return PageView.builder(
33 controller: pageController,
34 itemCount: favList.length,
35 itemBuilder: (context, index) {
36 return _ImageContainer(
37 image: favList[index] !.image,
38 );
39 },
40 );
41 },
42 //4
43 loading: () => CircularProgressIndicator(),
44 //5
45 error: () => _Error(),
46 );
47 },
48 )
49 ),
50 );
51 }
52}
53// 6
54class _ImageContainer extends StatelessWidget {
55 final String image;
56
57 const _ImageContainer({
58 Key ? key,
59 required this.image
60 }): super(key: key);
61
62 @override
63 Widget build(BuildContext context) {
64 return Container(
65 padding: EdgeInsets.all(16),
66 child: Image.asset(
67 image,
68 fit: BoxFit.contain,
69 ),
70 );
71 }
72}
73// 7
74class _Error extends StatelessWidget {
75 const Error({
76 Key ? key
77 }): super(key: key);
78
79 @override
80 Widget build(BuildContext context) {
81 return Container(
82 child: Text('Could not load data'),
83 );
84 }
85}
86We have to wrap our widget with StateNotifierProvider otherwise provider not found error will be thrown
When favList is empty, we display a message to tell users there are no items in the list
If not empty, we will display the list of items
During load state, CircularProgressIndicator will be displayed
During error state, we will display the error message, the Widget _Error in this case
_ImageContainer is a common Widget to display the images
_Error widget which returns an error message during the fetching process.
Run your app and you should see the favorite list page showing a circular indicator before displaying the list.
That’s all for this article. In the next one we will use the same state management method to connect the search screen to Spoonacular api.
Stay tuned for the next article.
CHECK OUT TOPCODER FLUTTER FREELANCE GIGS