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
2
3
4
5
6
7
# pubspec.yaml
dependencies:
freezed_annotation:
flutter_state_notifier: ^ 0.7 .0
dev_dependencies:
build_runner:
freezed:
We 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 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class User {}
class Loading extends User {}
class Success extends User {
final String username;
final String age;
Success({
required this.username,
required this.age
});
}
class Error extends User {
final Object error;
Error({
required this.error
});
}
We create our StateNotifier class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class UserController extends StateNotifier < User > {
UserController({
required MatchingState currentState,
}): super(Loading()) {
initialize();
}
void initialize() {
try {
//fetch some data
//update state to success when fetching is completed
state = Success(username: response, age: response);
} catch (e) {
//update status to error if some error is encountered
state = Error(error: e);
}
}
}
We consume our StateNotifier in the UI:
1 2 3 4 5 6 7
final state = context.watch < User > (); if (state is Loading) //show progress indicator if (state is Success) //display user details if (state is Error) //show error message
Instead 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.
1
2
3
4
5
6
7
8
9
10
11
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
@freezed
abstract class User with _$User {
const factory User.loading() = Loading;
const factory User.error() = Error;
const factory User.notLogged() = UserNotLogged
}
As 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 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class RecipeDetail {
final String image;
RecipeDetail({
required this.image,
});
}
Repository to fetch data:
final list = [
RecipeDetail(image: 'images/pasta.jpeg'),
RecipeDetail(image: 'images/cream.jpeg'),
RecipeDetail(image: 'images/food3.jpeg'),
];
class FavListRepository {
Future < List < RecipeDetail >> favList() async {
return await Future.delayed(
Duration(milliseconds: 500),
() => list,
);
}
}
We 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import 'package:freezed_annotation/freezed_annotation.dart';
part 'fav_list_view_model.freezed.dart';
@freezed
class FavListViewModel with _$FavListViewModel {
factory FavListViewModel({
@Default([]) List < RecipeDetail > favList,
}) = _FavListViewModel;
const factory FavListViewModel.loading() = Loading;
const factory FavListViewModel.error() = Error;
}
For 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class FavPageListController extends StateNotifier < FavListViewModel > {
FavPageListController(): super(FavListViewModel.loading()) {
initialize();
}
Future < void > initialize() async {
try {
final favList = await FavListRepository()
.favList();
state = FavListViewModel(favList: favList);
} catch (e) {
state = FavListViewModel.error();
}
}
}
The 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class FavListPage extends StatelessWidget {
FavListPage({
Key ? key
}): super(key: key);
late PageController pageController =
PageController(initialPage: 0, viewportFraction: 0.9);
@override
Widget build(BuildContext context) {
// 1
return
StateNotifierProvider < FavPageListController, FavListViewModel > (
create: (_) => FavPageListController(),
child: Scaffold(
appBar: AppBar(
title: Text(
'Favorite List',
),
),
body: Builder(
builder: (context) {
return context.watch < FavListViewModel > ()
.when(
(favList) {
//2
if (favList.isEmpty) {
return Center(
child: Text('Empty List'),
);
}
//3
return PageView.builder(
controller: pageController,
itemCount: favList.length,
itemBuilder: (context, index) {
return _ImageContainer(
image: favList[index] !.image,
);
},
);
},
//4
loading: () => CircularProgressIndicator(),
//5
error: () => _Error(),
);
},
)
),
);
}
}
// 6
class _ImageContainer extends StatelessWidget {
final String image;
const _ImageContainer({
Key ? key,
required this.image
}): super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16),
child: Image.asset(
image,
fit: BoxFit.contain,
),
);
}
}
// 7
class _Error extends StatelessWidget {
const Error({
Key ? key
}): super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: Text('Could not load data'),
);
}
}
We 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