I'm learning and building a Flutter app using BLoC pattern and in a lot of tutorials and repositories, I have seen people having a separate class for each state of the BLoC and others which have a single state class with all the properties defined.
Is there a standard BLoC way for defining state classes or is it a personal choice?
Example with multiple state classes
abstract class LoginState extends Equatable {
LoginState([List props = const []]) : super(props);
}
class LoginInitial extends LoginState {
#override
String toString() => 'LoginInitial';
}
class LoginLoading extends LoginState {
#override
String toString() => 'LoginLoading';
}
class LoginFailure extends LoginState {
final String error;
LoginFailure({#required this.error}) : super([error]);
#override
String toString() => 'LoginFailure { error: $error }';
}
Example with a single state class
#immutable
class MyFormState extends Equatable {
final String email;
final bool isEmailValid;
final String password;
final bool isPasswordValid;
final bool formSubmittedSuccessfully;
bool get isFormValid => isEmailValid && isPasswordValid;
MyFormState({
#required this.email,
#required this.isEmailValid,
#required this.password,
#required this.isPasswordValid,
#required this.formSubmittedSuccessfully,
}) : super([
email,
isEmailValid,
password,
isPasswordValid,
formSubmittedSuccessfully,
]);
factory MyFormState.initial() {
return MyFormState(
email: '',
isEmailValid: false,
password: '',
isPasswordValid: false,
formSubmittedSuccessfully: false,
);
}
MyFormState copyWith({
String email,
bool isEmailValid,
String password,
bool isPasswordValid,
bool formSubmittedSuccessfully,
}) {
return MyFormState(
email: email ?? this.email,
isEmailValid: isEmailValid ?? this.isEmailValid,
password: password ?? this.password,
isPasswordValid: isPasswordValid ?? this.isPasswordValid,
formSubmittedSuccessfully:
formSubmittedSuccessfully ?? this.formSubmittedSuccessfully,
);
}
#override
String toString() {
return '''MyFormState {
email: $email,
isEmailValid: $isEmailValid,
password: $password,
isPasswordValid: $isPasswordValid,
formSubmittedSuccessfully: $formSubmittedSuccessfully
}''';
}
}
Which one should be used when?
What's the advantage and disadvantage between both?
It's more of a personal choice / coding style but I agree that some way might be better than other depending on the scenario.
Note that Bloc doc now states the two methods.
There a few differences though. First, when using multiple state classes you have greater control about the fields of each state. For example if you know that an error can only happen in some case it might be better to isolate this in a specific class BlocSubjectError instead of having a class containing a nullable error that might or might not be there.
From a caller perspective the conditions are also a bit different, you might prefer one way or the other :
With multiple state classes
if(state is BlocSubjectLoading) {
// display loading indicator
}
else if (state is BlocSubjectError) {
// display errror
} else if (state is BlocSubjectSuccess) {
// display data
}
With single state class
if(state.isLoading) {
// display loading indicator
}
else if (state.error != null) {
// display errror
} else if (state.data != null) {
// display data
}
Though it might not look like a big difference, the second example allows for mixed states : you could have both an error and a data, or both being loading and having a previous data. It might or might be what you desire. In the first case the types make this more constrained which make this technique closer to representing the state of your app with types, a.k.a. Type Driven Development.
On the other hand, having multiple state classes might be cumbersome if they all declare the same fields because you will need to instantiate your states with all the parameters each time whereas using a single class you can just call a .copyWith() method to which you will only give changed parameters.
The single state class pattern is close to Triple Pattern (or Segmented State Pattern). You can find a comprehensive workshop that describes this pattern with Bloc here.
Related
I am still a beginner with BLoC architecture. So far the UI updates when using int, bool, and other basic data types. But when it comes to Maps it really confuses me. My code basically looks like this:
my state
enum TalentStatus { initial, loading, loaded, error }
class TalentState extends Equatable {
const TalentState({
required this.talentStatus,
this.selectedService = const {},
required this.talents,
this.test = 0,
});
final TalentStatus talentStatus;
final Talents talents;
final Map<String, Service> selectedService;
final int test;
TalentState copyWith({
TalentStatus? talentStatus,
Talents? talents,
Map<String, Service>? selectedService,
int? test,
}) =>
TalentState(
selectedService: selectedService ?? this.selectedService,
talentStatus: talentStatus ?? this.talentStatus,
talents: talents ?? this.talents,
test: test ?? this.test,
);
#override
List<Object> get props => [talentStatus, talents, selectedService, test];
}
my event
abstract class TalentEvent extends Equatable {
const TalentEvent();
#override
List<Object> get props => [];
}
class TalentStarted extends TalentEvent {}
class TalentSelectService extends TalentEvent {
const TalentSelectService(
this.service,
this.talentName,
);
final Service service;
final String talentName;
}
and my bloc
class TalentBloc extends Bloc<TalentEvent, TalentState> {
TalentBloc(this._talentRepository)
: super(TalentState(
talentStatus: TalentStatus.initial, talents: Talents())) {
on<TalentSelectService>(_selectService);
}
final TalentRepository _talentRepository;
Future<void> _selectService(
TalentSelectService event,
Emitter<TalentState> emit,
) async {
state.selectedService[event.talentName] = event.service;
final selectedService = Map<String, Service>.of(state.selectedService);
emit(
state.copyWith(
selectedService: selectedService,
),
);
}
}
whenever an event TalentSelectService is called BlocBuilder doesn't trigger, what's wrong with my code?
Your Service object must be comparable. One suggestion is that it extends Equatable. Either way it have to implement (override) the == operator and hashCode
The reason your BlocBuilder doesn't trigger is (probably) that it doesn't recognize that there has been a change in the Map.
Let's say I have few properties which describe a single context device (keepEmail is bool, email is String, deviceToken is String, themeMode is ThemeMode enumeration.
As they are belong to single domain object I think I need to create a DeviceState class (because I would like to save this object in web local storage as {"keepEmail": true, "email": "email#email.com", ...}
class DeviceState {
final bool keepEmail;
final String email;
final String deviceToken;
final ThemeMode themeMode;
// other constructors and methods
}
which in its turn is a part of ApplicationState:
class ApplicationState extends ChangeNotifier {
DeviceState get deviceState => localStorage['deviceState'];
set deviceState(DeviceState state) {
if(localStorage['deviceState'] != state) {
localStorage['deviceState'] = state;
notifyListeners();
}
}
// other states can be here
}
But here the chip is coming. Some of properties must be observables to refresh the navigator (I use package GoRouter which can listen Listenable to refresh its state) or user interface.
Namely change of deviceToken must launch router redirect (guard) system which checks if device token is set do something, and change of themeMode must refresh current theme.
So the question is if it is wise to combine all application states (which certainly can be persisted) like themes, languages, scroll positions, selected tabs etc to a single ApplicationState which is ChangeNotifier or create ChangeNotifier for all pieces of changed data and then aggregate them into single class:
class DeviceState extends ChangeNotifier {
String get deviceToken;
}
class LoginState extends ChangeNotifier {
String get accessToken;
}
class GoogleMapState extends ChangeNotifier {
MapStyle get mapStyle;
}
//... and many many other similar classes
class ApplicationState {
DeviceState deviceState;
AccessState accessState;
GoogleMapState mapState;
//...
}
and then use required Listenable in a specific place?
So I am a little lost how to implement corectly and I am ready to hear some ideas and advices.
How do you deal with "accepted null values" when you update a state in BLoC ?
I use the flutter_bloc package.
I have a form in which numeric variables are nullable so that I can check their validity before the form is submitted.
But when I emit a new state, I use state.copyWith(var1?, var2?)... so when a null value is used to update a parameter, the value is not updated.
To face that I use a custom FieldStatus enum for each field. In my form submission, I can check the status of each field. But this is a bit verbose... and it needs to use 2 values instead of 1 for each field, which is not very satisfying.
I can also force the value to be null according to the new value of its FieldStatus, but it is a bit tricky and not very satisfying.
How would you manage such a case ?
Here is what I did :
States :
part of 'phhfgroup_bloc.dart';
class PhhfGroupState extends Equatable
{
final double? height;
final FieldStatus heightStatus;
const PhhfGroupState({this.height, this.heightStatus = FieldStatus.initial});
#override
List<Object?> get props => [height, heightStatus];
PhhfGroupState copyWith({double? height, FieldStatus? heightStatus})
{
return PhhfGroupState(
height: height ?? this.height,
heightStatus: heightStatus ?? this.heightStatus
);
}
}
Events :
part of 'phhfgroup_bloc.dart';
abstract class PhhfGroupEvent extends Equatable
{
const PhhfGroupEvent();
#override
List<Object> get props => [];
}
class HeightChanged extends PhhfGroupEvent
{
const HeightChanged({required this.height});
final String height;
#override
List<Object> get props => [height];
}
Handler :
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:myapp/models/statuses.dart';
part 'phhfgroup_event.dart';
part 'phhfgroup_state.dart';
class PhhfGroupBloc extends Bloc<PhhfGroupEvent, PhhfGroupState>
{
PhhfGroupBloc() : super()
{
on<HeightChanged>(_mapHeightEventToState);
}
void _mapHeightEventToState(HeightChanged event, Emitter<PhhfGroupState> emit)
{
if(event.height.isEmpty)
{
emit(this.state.copyWith(
height: null,
heightStatus: FieldStatus.empty
));
}
else
{
double? height = double.tryParse(event.height);
if(height == null)
emit(this.state.copyWith(
height: null,
heightStatus: FieldStatus.nonnumeric
));
else emit(this.state.copyWith(
height: height,
heightStatus: FieldStatus.numeric
));
}
}
}
Thanks !
By using freeze, you could do as follow:
void main() {
var person = Person('Remi', 24);
// `age` not passed, its value is preserved
print(person.copyWith(name: 'Dash')); // Person(name: Dash, age: 24)
// `age` is set to `null`
print(person.copyWith(age: null)); // Person(name: Remi, age: null)
}
If you don't want to use another package, I suggest to add an argument for controlling nullable values.
class PhhfGroupState extends Equatable
{
final double? height;
final FieldStatus heightStatus;
const PhhfGroupState({this.height, this.heightStatus = FieldStatus.initial});
#override
List<Object?> get props => [height, heightStatus];
PhhfGroupState copyWith({double? height, FieldStatus? heightStatus, bool clearHeight = false})
{
return PhhfGroupState(
height: clearHeight == true ? null : height ?? this.height,
heightStatus: heightStatus ?? this.heightStatus
);
}
}
If you have a bunch of nullable fields, I would strongly recommend freeze, but for others, just add a flag for It.
Another way, you can use ValueGetter.
For example, password below:
#immutable
class LoginPageState {
const LoginPageState({
this.phone,
this.password,
});
final String? phone;
final String? password;
LoginPageState copyWith({
String? phone,
ValueGetter<String?>? password,
}) {
return LoginPageState(
phone: phone ?? this.phone,
password: password != null ? password() : this.password,
);
}
}
Set password to null:
void resetPassword() {
final newState = state.copyWith(password: () => null);
emit(newState);
}
You don't need any extra flag.
Thanks a lot because it is exactly what I need ! Avoid lots of boilerplate code to achieve this simple goal...
So to achieve what I need I will use :
freeze to help manage state / events
my FieldStatus enum to make some informations available in the view
e.g. I have class ProfileModel with bunch of fields
many of them don't have default values unless they're initialising when I get user info from backend
with riverpod I need to write something like
final profileProvider = StateNotifierProvider((ref) => ProfileState());
class ProfileState extends StateNotifier<ProfileModel> {
ProfileState() : super(null);
}
I understand I need to pass something like ProfileState.empty() into super() method instead passing null
but in this case I have to invent default values for every ProfileModels fields
this sounds weird for me, I don't want to break my head to care about empty or default state of EVERY model in project
in my example there are no default values for user name, age etc
this is pure immutable class
what I'm doing wrong or missing?
or I can declare model as nullable extends StateNotifier<ProfileModel?>
but I'm not sure is this a good way
It is fine to use the StateNotifier with a nullable model. If you semantically want to indicate the value can be actually absent, I would say that that having null is alright.
However, what I usually do and what I think is better, is create a state model that contains the model, but also properties that relate to the different states the app could be in.
For example, while fetching the data for the model from an API, you might want to have a loading state to show a spinner in the UI while waiting for the data to be fetched. I wrote an article about the architecture that I apply using Riverpod.
A simple example of the state model would be:
class ProfileState {
final ProfileModel? profileData;
final bool isLoading;
ProfileState({
this.profileData,
this.isLoading = false,
});
factory ProfileState.loading() => ProfileState(isLoading: true);
ProfileState copyWith({
ProfileModel? profileData,
bool? isLoading,
}) {
return ProfileState(
profileData: profileData ?? this.profileData,
isLoading: isLoading ?? this.isLoading,
);
}
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is ProfileState &&
other.profileData == profileData &&
other.isLoading == isLoading;
}
#override
int get hashCode => profileData.hashCode ^ isLoading.hashCode;
}
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],
);