What are cubits A Cubit is a simple and efficient way to handle state management in...
A Cubit is a simple and efficient way to handle state management in Flutter apps.It's a part of the broader Bloc (Business Logic Component) library, which helps you manage your app's state in a structured and organized manner.
State Management: A Cubit helps you manage the different states your app can be in. This is useful for handling data loading, user interactions, and more.
Simplicity: Cubits are designed to be simple and easy to understand. They are a great choice for small to medium-sized applications or when you want to avoid the complexity of full-blown Blocs.
Events and States: In a Cubit, you define a series of events that can trigger state changes. For example, you might have an event to fetch data from an API, and the Cubit can have states like "loading," "success," or "error" to represent the various stages of the data-fetching process.
UI Integration: Cubits update the user interface based on changes in state. This ensures that your app's UI always reflects the current state of your Cubit.
We're going to hit the json placeholder API and display the list of users in our app.We'll use the dio package for handling the network request.
In your pubspec.yaml
file, include these packages under the dependencies:
bloc
and flutter_bloc
packages for state managementdio
package for making HTTP requestsfreezed_annotation
for auto-generating Cubit statesjson_annotation
for generation of json de/serialization methodsdependencies:
flutter:
sdk: flutter
bloc: ^8.1.2
flutter_bloc: ^8.1.3
dio: ^5.3.3
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
Also add these packages under the dev_dependencies.They help with code generation for the cubits and also with json serialization.
dev_dependencies:
freezed: ^2.4.2
json_serializable: ^6.7.1
build_runner: ^2.4.6
This is how we will structure our files for easy maintainability.
your_project_name/
lib/
cubits/
users_cubit.dart
users_states.dart
models/
user.dart
screens/
users_page.dart
main.dart
In the lib/models/user.dart
file,create a freezed User
class.
The@freezedannotation is used to generate the boilerplate code for the User
class. This includes the constructor, copyWith
method, toString
method, and operator== method.
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String username,
required String email,
required String phone,
required String website,
}) = _User;
factory User.fromJson(Map json) => _$UserFromJson(json);
}
The User class also includes a fromJson
factory method that is used to create a new instance of the User
class from a JSON object. This method is generated using the json_serializable
package, which is a companion package to freezed_annotation
.
After doing that you can run this command in your terminal to generate the code:
dart run build_runner build --delete-conflicting-outputs
In the lib/cubits/users_states.dart
we create a UserStates
class.
The UserStates
class is used to represent the different states that the User
list can have in our application,that is, initial
state, loading
state, error
state and success
state.
The class is also marked with the@freezedannotation to generate the boilerplate code for the Cubit.
part of 'users_cubit.dart';
@freezed
class UsersState with _$UsersState {
const factory UsersState.initial() = _Initial;
const factory UsersState.loading() = _Loading;
const factory UsersState.success(List users) = _Success;
const factory UsersState.error(String errorMessage) = _Error;
}
After this you can run the build_runner command above,to autogenerate the methods and remove the errors.
After defining all the possible states that our app can have,it's time to bring everything in order - that's what a cubit basically does.
First create a UsersCubit class that extends Cubit from the bloc package:
part 'users_state.dart';
part 'users_cubit.freezed.dart';
class UsersCubit extends Cubit {
UsersCubit() : super(const UsersState.initial());
}
The cubit should initally contain an instance of UsersState.initial() passed in it's constructor, which is the initial state before the API calls begin to happen.
Next,we will define a method fetchUsers() in which we will contact the API:
fetchUsers() async {
try {
emit(const UsersState.loading());
Dio dio = Dio();
final res = await dio.get("https://jsonplaceholder.typicode.com/users");
if (res.statusCode == 200) {
final users = res.data.map((item) {
return User.fromJson(item);
}).toList();
emit(UsersState.success(users));
} else {
emit(
UsersState.error("Error loading users: $"),
);
}
} catch (e) {
emit(
UsersState.error("Error loading users: $"),
);
}
}
fetchUsers
method first emits a UsersState.loading()
state to indicate that the user list is being loadedfromJson
factory method of the User class. The success state is then emitted with the list of User objects.The method should be writen inside of the UsersCubit
class. For better and cleaner code,the API call would be separated in a repository file but let's just keep it simple for now.
This is how the cubit finally looks like:
part 'users_state.dart';
part 'users_cubit.freezed.dart';
class UsersCubit extends Cubit {
UsersCubit() : super(const UsersState.initial());
fetchUsers() async {
try {
emit(const UsersState.loading());
Dio dio = Dio();
final res = await dio.get("https://jsonplaceholder.typicode.com/users");
if (res.statusCode == 200) {
final users = res.data.map((item) {
return User.fromJson(item);
}).toList();
emit(UsersState.success(users));
} else {
emit(
UsersState.error("Error loading users: $"),
);
}
} catch (e) {
emit(
UsersState.error("Error loading users: $"),
);
}
}
}
Make sure you import all the necessary packages into the file
Now it's time to consume our UsersCubit
from the UI and show the different states as defined.
In the lib/main.dart
file at the root MyApp class,we shall have the MaterialApp() class,whose home property will be aBlocProvider().
ABlocProvidertakes in a create
function,that is responsible for creating an instance of a Cubit and a child
Widget which will have access to that instance through it's context.
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
useMaterial3: true,
),
home: BlocProvider(
create: (context)=> UsersCubit(),
child: const UsersPage(),
),
);
}
}
We then create a Stateless Widget called UsersPage
in the lib/screens/users_page.dart
file.The page has a simple AppBar and for the body we use a BlocBuilder.
BlocBuilder takes in a cubit(UsersCubit in our case) and a state(UsersState).It then handles the building of a widget in response to the cubit's current state.
class UsersPage extends StatelessWidget {
const UsersPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: const Text("Users"),
),
body: BlocBuilder(
builder: (context, state) {
//UI is built per the state
},
),
);
}
}
The builder function has astate.whenmethod which is used to handle the different states of the UsersCubit
:
body: BlocBuilder(
builder: (context, state) {
return state.when(
initial: () => Center(
child: ElevatedButton(
child: const Text("Get Users"),
onPressed: () => context.read().fetchUsers()
),
),
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: ((errorMessage) => Center(child: Text(errorMessage),)),
success: (users) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(users[index].name),
subtitle: Text(users[index].email),
);
},
);
},
);
},
),
initial
, a Center widget with an ElevatedButton is returned. If the button is pressed, the fetchUsers
method of the UsersCubit
is called to load the user list.loading
, a Center widget with a CircularProgressIndicator is returned to indicate that the user list is being loaded.error
, a Center widget with a Text widget that displays the error message is returned.success
, a ListView.builder widget is returned to display the list of users.With that,all the states in our cubit are taken care of effectively.
Initial State
Loading State
Success State
Error State
With Cubits you just have to define your states then show the different UI's based on the current state of that cubit,that simple????