Should I use multiple classes for bloc states or one class with multiple constructors? - flutter

I have started learning bloc recently and I noticed some tutorials use multiple classes for states and others just use one class with multiple named constructors.
Example for multiple classes:
abstract class ProductsState {}
class ProductsInitial extends ProductsState {}
class ProductsLoading extends ProductsState {}
class ProductsSuccess extends ProductsState {
final List<Products> products;
ProductsSuccess(this.products);
}
class ProductsError extends ProductsState {
final String error;
ProductsError(this.error);
}
Example for multiple constructors:
enum AuthenticationStates {
initial,
success,
error,
}
class AuthenticationState {
final AuthenticationStates state;
final Authentication model;
final String msg;
const AuthenticationState._(
{this.state = AuthenticationStates.initial,
this.model,
this.msg = ''});
const AuthenticationState.initial() : this._();
const AuthenticationState.success(Authentication mdl)
: this._(model: mdl, state: AuthenticationStates.success);
const AuthenticationState.error(String m)
: this._(state: AuthenticationStates.error, msg: m);
}
Which one is better to use?

In my projects, I use the first way since different state types are clear, you can later build your UI based on that e.g. if (state is ProductsLoading) { show loader }.
You can also generate such classes by using the freezed package (https://pub.dev/packages/freezed). When using it, you would define a single class with different factory constructors (similar to your second option), but the generated code would support the first option you've defined, e.g.:
#freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String message]) = ErrorDetails;
}
Later, in the UI layer, you could use helper methods like map/maybeMap/when/maybeWhen to build the corresponding view based on state, e.g.:
var union = Union(42);
print(
union.when(
(int value) => 'Data $data',
loading: () => 'loading',
error: (String? message) => 'Error: $message',
),
); // Data 42

I was asking myself the same question.
"The moment you check the concrete type of an instance in your logic, know you're not using polymorphism correctly." - Back in the university.
In the documentation of BloC they use one class with enum in all examples(or extra status class),
the enum will be composed of something similar to SUCCESS FAILURE etc.
Check the PostState mentioned in BloC documentation as example:
https://bloclibrary.dev/#/flutterinfinitelisttutorial?id=post-states
Nevertheless, the multiple sub-classes for state is a commonly used practice among flutter devs. It looks wrong for me, but who knows...

Related

Dart freezed same field on all constructors

I'm trying to make a state class for my todo bloc, and I want to keep the loaded todos when I start refreshing. Is there a better way to do this, basically having a mutual field in all the constructors of the class using freezed package
#freezed
class TodoState with _$TodoState {
const factory TodoState.loading(List<Todo> todos) = TodoStateLoading;
const factory TodoState.loaded(List<Todo> todos) = TodoStateLoaded;
const factory TodoState.error(List<Todo> todos, String message) = TodoStateError;
}
I can already use it like this, but i would like to just call state.todos and not having to check for its type:
TodoState state = TodoStateLoaded([/*example*/]);
state.todos // ERROR
(state as TodoStateLoaded).todos // OK
As per this comment freezed do not support super classes. The closest solution that might work for you is this:
// create mixin with getter of properties that you would like to share between states
mixin TodoMixin {
List<Todo> get todos;
}
#freezed
// add mixin to your class with state
class TodoState with TodoMixin, _$TodoState {
const TodoState._(); // add private constructor
//override getter with map function that will return data for each state
#override
List<Todo> get todos {
return map(
loading: (state) => state.todos,
loaded: (state) => state.todos,
error: (state) => state.todos,
);
}
const factory TodoState.loading(List<Todo> todos) = TodoStateLoading;
const factory TodoState.loaded(List<Todo> todos) = TodoStateLoaded;
const factory TodoState.error(List<Todo> todos, String message) = TodoStateError;
}
It will create common getter for state and when you will introduce new state map method will show you, that you need handle new state in getter. Add another property in mixin and you will need override it too.
To use it you can call it in cubit:
state.todos; or in view: context.read<TodoCubit>().state.todos;

when should we use freezed as sealed classes or constructor?

Which way to use freezed library with bloc ? first one as a sealed classes, the other is a constructor.
First way
abstract class HomeState with _$HomeState {
const factory HomeState.initial() = _Initial;
const factory HomeState.addNewNoteButtonClicked(#Default(false) bool isClicked) = AddNewNoteClicked;
factory HomeState.addNewNote( Note value) = AddNewNote;
}
Second Way:
abstract class HomeState with _$HomeState {
const factory HomeState({
required Note value,
required bool isClicked,
}) = AddNewNoteClicked;
factory HomeState.init() => HomeState(
value: Note(value: ''),
isClicked: false,
);
}
TL;DR: I think there is no correct way, just what works for you.
When using freezed, every factory constructor generates a separate class. Together with that, it generates some convenience methods, like map/maybeMap/when/maybeWhen. This is very convenient when your BLoC has obvious and different states and you want to handle your UI accordingly. For instance: initial, loadInProgress, loadSuccess, loadFailure. Then, in your UI, you could use something like:
class Example extends StatelessWidget {
const Example();
#override
Widget build(BuildContext context) {
return BlocBuilder<ExampleCubit, ExampleState>(
builder: (_, state) => state.maybeWhen(
loadInProgress: () => const LoaderView(),
loadFailure: () => const ErrorView(),
loadSuccess: (categories) => const SomeView(),
orElse: () => const SizedBox(),
),
);
}
}
However, it also brings some inconvenience when you need to take data from the specific state: you must check if the state is a specific one and only then you can process with your code, e.g:
if (state is ExampleStateSuccess) {
...
}
In such cases, when you need to have just a single state but a lot of different properties (a good example would be form validation when you store all the field properties in your BLoC and you want to validate/update them, submit the form later) it is better to use a single state with properties. By better, I mean it's just easier this way.

Why when i use a class in dart with equatable and just a list as property the copyWith method return the same object, same hascode

Im using bloc and it was working as expected but today i notice a strage behaviour when i was sending the same state (RefreshState) using copyWith, the state wasnt trigger after second call. then i did a test creating two objects and compared them but the result was they are the same object, very odd.
So why is this happen?, this is my class:
class Model extends Equatable {
final List<Product> mostBuyProducts;
const Model({
this.mostBuyProducts,
});
Model copyWith({
List<Product> mostBuyProducts,
}) =>
Model(
mostBuyProducts: mostBuyProducts ?? this.mostBuyProducts,
);
#override
List<Object> get props => [
mostBuyProducts,
];
}
and then i use the CopyWith method like (inside the bloc):
Stream<State> _onDeleteProduct(OnDeleteProduct event) async* {
state.model.mostBuyProducts.removeWhere((p) => p.id == event.id);
var newMostBuyProducts = List<Product>.from(state.model.mostBuyProducts);
final model1 = state.model;
final model2 = state.model.copyWith(mostBuyProducts: newMostBuyProducts);
final isEqual = (model1 == model2);
yield RefreshState(
state.model.copyWith(mostBuyProducts: newMostBuyProducts));
}
isEqual return true :/
BTW this is my state class
#immutable
abstract class State extends Equatable {
final Model model;
State(this.model);
#override
List<Object> get props => [model];
}
Yes because lists are mutable. In order to detect a change in the list you need to make a deep copy of the list. Some methods to make a deep copy are available here : https://www.kindacode.com/article/how-to-clone-a-list-or-map-in-dart-and-flutter/
Using one such method in the solution below! Just change the copyWith method with the one below.
Model copyWith({
List<Product> mostBuyProducts,
}) =>
Model(
mostBuyProducts: mostBuyProducts ?? [...this.mostBuyProducts],
);

using Equatable class with flutter_bloc

Why do we need to use the Equatable class with flutter_bloc? Also, what do we use the props for? Below is sample code for making a state using the bloc pattern in Flutter.
abstract class LoginStates extends Equatable{}
class LoginInitialState extends LoginStates{
#override
List<Object> get props => [];
}
For comparison of data, we required Equatable. it overrides == and hashCode internally, which saves a lot of boilerplate code. In Bloc, we have to extend Equatable to States and Events classes to use this functionality.
abstract class LoginStates extends Equatable{}
So, that means LoginStates will not make duplicate calls and will not going to rebuild the widget if the same state occurs.
Define State:
class LoginInitialState extends LoginStates {}
Define State with props:
props declared when we want State to be compared against the values which declared inside props List
class LoginData extends LoginStates {
final bool status;
final String userName;
const LoginData({this.status, this.userName});
#override
List<Object> get props => [this.status, this.userName];
}
If we remove the username from the list and keep a list like [this.status], then State will only consider the status field, avoiding the username field. That is why we used props for handling State changes.
Bloc Stream Usage:
As we extending State with Equatable that makes a comparison of old state data with new state data. As an example let's look at the below example here LoginData will build a widget only once, which will avoid the second call as it is duplicated.
#override
Stream<LoginStates> mapEventToState(MyEvent event) async* {
yield LoginData(true, 'Hello User');
yield LoginData(true, 'Hello User'); // This will be avoided
}
Detail Blog: https://medium.com/flutterworld/flutter-equatable-its-use-inside-bloc-7d14f3b5479b
We are using the Equatable package so that we can compare instances of classes without having to manually override "==" and hashCode.
Equatable class allows us to compare two object for equality.
This is the equatable example. Let's say we have the following class:
class Person {
final String name;
const Person(this.name);
}
We can create instances of Person like so:
void main() {
final Person bob = Person("Bob");
}
Later if we try to compare two instances of Person either in our production code or in our tests we will run into a problem.
print(bob == Person("Bob")); // false
In order to be able to compare two instances of Person we need to change our class to override == and hashCode like so:
class Person {
final String name;
const Person(this.name);
#override
bool operator ==(Object other) =>
identical(this, other) ||
other is Person &&
runtimeType == other.runtimeType &&
name == other.name;
#override
int get hashCode => name.hashCode;
}
Now if we run the following code again:
print(bob == Person("Bob")); // true
it will be able to compare different instances of Person.
So you don't have to waste your time writing lots of boilerplate code when overrides "==" and hashCode.
Use Equatable like
class Person extends Equatable
In bloc case; if you try to use bloc with mutable state you will face with problems without Equatable. It makes resources immutable reduce performance. It’s more expensive to create copies than to mutate a property.
If it is not clear to you that I tried to explain, reading this could help you.
Equatable overrides == and hashCode for you so you don't have to waste your time writing lots of boilerplate code.
There are other packages that will actually generate the boilerplate for you; however, you still have to run the code generation step which is not ideal.
With Equatable there is no code generation needed and we can focus more on writing amazing applications and less on mundane tasks.
and the props is a getter of equatable that takes the properties that we want to
although it needs no focus on it i only putted properties to props getter
it is not that Important but i suggest you to read more about it in here

Where and when should I store values in BLoC itself or its state

Context
I come from Redux and I am learning the BLoC pattern for global state management. I am having troubles defining where should I store values as properties inside the BLoC class, and when I should store values inside States.
Use case
I have a home page where I show some Ads, depending on its category (category should never be null). I implemented an AdsBloc like this:
class AdsBloc extends Bloc<AdsEvent, AdsState> {
final AdsRepository repository;
AdsBloc({#required this.repository})
: super(AdsInitial(category: Category.DOGS));
#override
Stream<AdsState> mapEventToState(
AdsEvent event,
) async* {
// my code
}
}
And these are my AdsState:
abstract class AdsState {
final Category category;
AdsState({this.category});
}
class AdsInitial extends AdsState {
AdsInitial({category}) : super(category: category);
}
class AdsSuccess extends AdsState {
final PaginatedAds paginatedAds;
AdsSuccess({this.paginatedAds, category}) : super(category: category);
}
class AdsFailure extends AdsState {
AdsFailure({category}) : super(category: category);
}
Problem
Because of the way I implemented the pattern, I need to pass the category every time I change the state.
Solution?
So I was thinking if I can consider the category property to be of AdsBloc and remove it from state, this way I can get a better control of this property.
Implement "category" as a parameter for the event that is triggering ad loading process. This way, for example, you will tell BLoC to "LoadAds" with "category: Category.DOGS".
class LoadAds extends AdsEvent {
final Category category;
LoadAds({
#required this.category,
});
#override
List<Object> get props => [category];
}
This way you will be able to use single bloc instance to load different ad types when needed. Load DOGS first, 2 minutes later load CATS instead of dogs.
If you do not need this ability - then defining category inside bloc itself is perfectly fine.