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
Related
I want to update a state of an object and notify to update the UI accordingly.
For example
import 'package:riverpod_annotation/riverpod_annotation.dart';
class CounterState {
int value;
bool isCompleted;
CounterState({this.value = 0, this.isCompleted = false});
}
#riverpod
class CounterProvider extends _$CounterProvider {
#override
CounterState build() {
return CounterState;
}
void setValue(int newValue) {
state.value = newValue;
if (newValue == 1) {
state.isCompleted = true;
}
}
}
I use CounterState to contain all the necessary state of for 1 page. As this method doesn't work, and only works if I use this
if (newValue == 1) {
state = CounterState(value: newValue, isCompleted: true);
}
As if the state object become more and more complex, I want to keep other variable in the state remain the same, and only update the one I need, for the example above I can use something like this
if (___) {
state = CounterState(value: state.value, isCompleted: true);
}
But it is going to be difficult to update the state if CounterState have 10 variables or more.
Is there a way to overcome this?
Try implementing the copyWith method
class CounterState {
int value;
bool isCompleted;
CounterState({
this.value = 0,
this.isCompleted = false
});
CounterState copyWith({ int? value, bool? isCompleted }) => CounterState(
value: value ?? this.value,
isCompleted: isCompleted ?? this.isCompleted
);
}
With that done, you can just do:
state = state.copyWith(value: newValue, isCompleted: true, ...);
And just specify the fields you would like to update.
My problem is that when I'm calling a Cubit function it changes a value before I hit emit. Because of that, the Cubit cannot detect that there was a change in the state and the emit() won't work.
I solved the problem by creating a random number each time the function is called so that the cubit can recognize the state change. I just want to know what I'm doing wrong here. I'm already using the Equatable package.
part of 'survey_cubit.dart';
abstract class SurveyState extends Equatable {
const SurveyState({
required this.business,
required this.locals,
this.local = Local.en,
this.status = BusinessStatus.setup,
this.review = Review.empty,
this.questionIndex = 0,
this.answersId = const [],
this.questionsId = const [],
this.random = 0.0,
});
final Business business;
final List<Local> locals;
final Local local;
final BusinessStatus status;
final Review review;
final int questionIndex;
final List<int> answersId;
final List<int> questionsId;
final double random;
#override
List<Object> get props => [
business,
locals,
local,
status,
review,
questionIndex,
answersId,
questionsId,
random,
];
SurveyState copyWith({
Business? business,
List<Local>? locals,
Local? local,
BusinessStatus? status,
Review? review,
int? questionIndex,
List<int>? answersId,
List<int>? questionsId,
double? random,
});
}
class SurveyInitial extends SurveyState {
const SurveyInitial({
required super.business,
required super.locals,
super.local = Local.en,
super.status = BusinessStatus.setup,
super.review = Review.empty,
super.questionIndex = 0,
super.answersId = const [],
super.questionsId = const [],
super.random = 0.0,
});
#override
SurveyState copyWith({
Business? business,
List<Local>? locals,
Local? local,
BusinessStatus? status,
Review? review,
int? questionIndex,
List<int>? answersId,
List<int>? questionsId,
double? random,
}) =>
SurveyInitial(
business: business ?? this.business,
locals: locals ?? this.locals,
local: local ?? this.local,
status: status ?? this.status,
review: review ?? this.review,
questionIndex: questionIndex ?? this.questionIndex,
answersId: answersId ?? this.answersId,
questionsId: questionsId ?? this.questionsId,
random: random ?? this.random,
);
}
class SurveyCubit extends Cubit<SurveyState> {
SurveyCubit(DeviceInfo deviceInfo)
: super(
SurveyInitial(
business: deviceInfo.business!,
locals: deviceInfo.locals,
),
);
void onRemoveReview(int questionId) {
final Review review = state.review;
review.reviewedQuestions.removeWhere(
(element) => element.questionId == questionId,
);
final List<int> questionsId = state.questionsId;
questionsId.remove(questionId);
emit(
state.copyWith(
review: review,
answersId: [],
questionsId: questionsId,
random: Random().nextDouble(),
),
);
print(state.questionsId);
}
}
In your cubit, you assign state.questionsId (the old state's list) to a new variable. This doesn't create a new list; it just adds a new reference to the old one. The object ID is still the same. When you emit the new state, Equatable looks at the object ID and sees that it's identical and thinks the two states are the same.
From the Bloc documentation:
Equatable properties should always be copied rather than modified. If
an Equatable class contains a List or Map as properties, be sure to
use List.from or Map.from respectively to ensure that equality is
evaluated based on the values of the properties rather than the
reference.
final List<int> questionsId = state.questionsId; should be final List<int> questionsId = List.from(state.questionsId);.
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.
I am getting bool value timerState through the constructor. When I print the value I get proper results (true or false).
I need to call the function _controller.start() when timerState() == true. However when it is true I get the error: "Failed assertion: boolean expression must not be null".
Surprisingly, when timerState is false, I am not getting this error.
Please help, where can be a problem.
Below is my code:
class PieChart extends StatefulWidget {
final String userId;
final String userName;
final bool timerState;
final Key key;
PieChart(
this.userId,
this.userName,
this.timerState,
{this.key});
#override
State<StatefulWidget> createState() => PieChartEmotionsState(
userId,
userName,
timerState,
);
}
class PieChartEmotionsState extends State {
final String userId;
final String userName;
final bool timerState;
final Key key;
PieChartEmotionsState(
this.userId,
this.userName,
this.timerState,
{this.key});
#override
Widget build(BuildContext context) {
CountDownController _controller = CountDownController();
int _duration = 10;
bool getTimerState() {
print('PIE CHART timerState: $timerState');
return timerState == true;
}
if (getTimerState()) {
_controller.start();
}
return Container(...
I have obviously tried a simpler way:
if(timerState) _controller.start();
even this:
if(timerState ?? false) _controller.start();
But it seems not to have any impact on the issue. I am getting the same error...
What could be the solution?
Many thanks in advance for your support.
Dan
You don't need co create copies of your widget classes variables in the State class. Access them as widget.variable
class PieChart extends StatefulWidget {
final String userId;
final String userName;
final bool timerState;
final Key key;
PieChart(
this.userId,
this.userName,
this.timerState,
{this.key});
#override
State<PieChart> createState() => _PieChartState();
}
class _PieChartState extends State<PieChart> {
bool getTimerState() {
print('PIE CHART timerState: ${widget.timerState}');
return widget.timerState == true;
}
#override
Widget build(BuildContext context) {
CountDownController _controller = CountDownController();
int _duration = 10;
if (getTimerState()) {
_controller.start();
}
return Container(...
and make sure you initialize timerState when creating your PieChart widget as it's declared final.
PieChart('userId', 'userName', true);
If you don't initialize it, it's set to null and the error you get is because you are checking if null == true in your getTimerState() method.
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.