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.
Related
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
I'm trying to pass a value of a variable from a StatefulWiget to another StatefulWidget
class InputFieldCheckTick extends StatefulWidget {
double timbreFiscaleFournisseur = 0.000;
bool exoTVA = false;
.
.
.
value: isTicked,
onChanged: (value) async {
await setState(() {
isTicked = value;
if (isTicked == false) {
widget.exoTVA = false;
} else {
widget.exoTVA = true;
}
});
.
.
.
value: isTicked,
onChanged: (value) async {
await setState(() {
isTicked = value;
if (isTicked == false) {
widget.exoTVA = false;
} else {
widget.exoTVA = true;
}
});
and i'm trying to pass the values of exoTVA and timbreFiscaleFournisseur here :
setState(() {
future = ajoutFournisseur(
numeroFournisseur.text,
addressFournisseur.text,
matriculeFiscaleFournisseur.text,
raisonSocialeFournisseur.text,
paysFournisseur.text,
villeFournisseur.text,
InputFieldCheckTick()
.timbreFiscaleFournisseur,
InputFieldCheckTick().exoTVA);
});
I think you are creating an StatefulWidget which contains a Final property called exoTVA, something like:
class MyWidget extends StatefulWidget {
final bool exoTVA;
[...]
Because you are calling widget.exoTVA which refers to stateful related widget, and there is your mistake. You can't change widget.exoTVA for the way widgets are build. What you can do is to do this:
class _MyWidgetState extends State<MyWidget> {
// Here you designate an instance value for your widget property
bool? _exoTVA;
// On initState you define from parent widget, I do this all the time
#override
initState((){
super.initState();
_exoTVA = widget.exoTVA;
});
[...] //Continue with the rest of your widget
Also when you call those changes in setState, change widget.exoTVA to _exoTVA, and to finish you can call that var like this:
setState(() {
future = ajoutFournisseur(
numeroFournisseur.text,
addressFournisseur.text,
matriculeFiscaleFournisseur.text,
raisonSocialeFournisseur.text,
paysFournisseur.text,
villeFournisseur.text,
InputFieldCheckTick()
.timbreFiscaleFournisseur,
_exoTVA);//Changed this line
});
For your reference, check out StatefulWidget class
I have a simple counter application made by Flutter using Bloc. The idea is after the user presses the increase button, it will delay for 2 seconds, show loading, then increase/decrease the value.
The counter bloc contains 2 states, CounterValue and CounterLoading. However, whenever I increase, the bloc starts creating a new CounterLoading object with a value of 0. To deal with this, I have to pass the current value at the CounterValue state to CounterLoading state, and after 2 seconds, I have to pass again the current value of Loading state to CounterValue state to increase the value. Hence this seems to be pretty redundant and confused when it comes to real situations where we have multiple states in the middle which don't need data while the first and last state emitted are dependent.
What is the best practice to store temp data across states using bloc?
counter_state.dart
class CounterState {
int value = 0;
}
class CounterLoading extends CounterState {}
class CounterValue extends CounterState {}
counter_bloc.dart
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue()) {
on<IncrementCounter>(
(event, emit) async {
emit(CounterLoading()..value = state.value);
await Future.delayed(const Duration(seconds: 2));
emit(CounterValue()..value = state.value + 1);
},
);
}
I would recommend investigating immutable states, which is very convenient while using BLoC. It means that instead of directly changing property values in the state, you are rather creating a new state object and replacing the previous state.
For this specific problem, I would recommend you to have a more complex state instead of having two separate states for Loading/Value. For instance:
State:
class CounterState {
const CounterState({
this.value = 0,
this.isLoading = false
});
final int value;
final bool isLoading;
}
BLoC:
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(CounterValue()) {
on<IncrementCounter>(
(event, emit) async {
final cur = state.value;
emit(CounterState(value: cur, isLoading: true));
await Future.delayed(const Duration(seconds: 2));
emit(CounterState(value: cur + 1, isLoading: false));
},
);
}
When state is more complicated than just 2 values, you should combine the use of Equatable package and copyWith method in your state.
Equatable makes it easy to tell that stateA != StateB. It's a replacement for manually overriding the == operator in your state class.
copyWith just returns a new state with zero or more of its values changed. It should be used basically every time you want to emit a new state.
State:
class CounterState extends Equatable {
const CounterState({
this.value = 0,
this.isLoading = false
});
final int value;
final bool isLoading;
#override
List<Object> get props => [value, isLoading];
CounterState copyWith({
int? value,
bool? isLoading,
}) {
return CounterState(
value: value ?? this.value,
isLoading: isLoading ?? this.isLoading,
);
}
}
Cubit:
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(const CounterState());
Future<void> increment() async {
emit(state.copyWith(isLoading: true));
await Future<void>.delayed(const Duration(seconds: 2));
emit(state.copyWith(value: state.value + 1, isLoading: false));
}
}
And for even larger apps where you might use custom objects, not just integers, strings and booleans, I recommend you take a look at freezed for creating models to include in your state with their own copyWith methods, etc. automatically generated.
Emits would look something like this for an AuthState with a User model:
void updateUserName(String newName) {
emit(state.copyWith(user: state.user.copyWith(name: newName)));
}
So I have written this code below to make an Icon Stateful inside a Stateless widget.
class IconState extends StatefulWidget {
final bool isSelected;
IconState({
this.isSelected,
});
_IconState state; // this is not final because I need to assign it below
void toggle() {
state.change();
}
#override
_IconState createState() => state = new _IconState(
isSelected: this.isSelected,
);
}
class _IconState extends State<IconState> {
_IconState({
this.isSelected,
});
bool isSelected = false;
Widget _unSelected = Icon(
null,
);
Widget _selected = Icon(
Icons.check_outlined,
color: Colors.red,
);
void change() {
setState(() {
this.isSelected = this.isSelected == true ? false : true;
});
}
Icon evaluate() {
if (isSelected) {
return _selected;
}
return _unSelected;
}
#override
Widget build(BuildContext context) {
return evaluate();
}
}
To update the state of the Icon, I call the toggle() method from my Stateless widget.
Dart is giving me a non-final instance warning inside an #immutable class, but I am unable to find a workaround for this.
I have tried following:
final _IconState state = new _IconState(
isSelected: this.isSelected, // throws an error => Invalid reference to 'this' expression.
);
also this, but doesn't work either
final _IconState state;
IconState({this.isSelected}) {
this.state = new _IconState(
isSelected: this.isSelected,
);
};
Is there a workaround?
I would put the isSelected boolean inside an external state management class, then you can return 2 separate widgets in response to the change. Otherwise you would have to change the state inside of the widget where the icon will be displayed. Something like this:
class IconState extends ChangeNotifier{
bool _isSelected;
//any other needed state
bool get isSelected => _isSelected;
void changeIsSelected(bool selected) {
_isSelected = selected;
notifyListeners();
}
}
Then use ChangeNotifierProvider to to inject the state and call the change method.
final iconStateProvider = ChangeNotifierProvider((ref) => IconState());
Now, you can use iconStateProvider to access the state and methods. You will need a Builder or Consumer widget to listen for changes to the state.
Consumer( builder: (context, watch, child) {
final iconState = watch(iconStateProvider);
if (iconState.isSelected) {
return Icon();
} else {
return null;
}
This is using the Riverpod library, which is only 1 of many external state management libraries. I recommend watching tutorials on YouTube on different libraries and pick one that best suits you.
Whenever I call the toggleLocked event, the BlocBuilder does not rebuild the widget.
I have looked around a lot on the internet and found this explanation: https://stackoverflow.com/a/60869187/3290471
I think that somewhere I incorrectly use the equatable package which results in the fact that the BlocBuilder thinks nothing has changed (while is has).
I have read the FAQ from the Bloc libray and the three provided solution (props for equatable / not reusing the same state / using fromList) seem to not fix the problem.
My Cubit:
class LockCubit extends Cubit<LockState> {
LockCubit({#required this.repository})
: assert(repository != null),
super(LockInitial());
final LocksRepository repository;
Future<void> fetch() async {
try {
final locks = await repository.fetchLocks();
emit(LocksDisplayed().copyWith(locks));
} on Exception {
emit(LockError());
}
}
Future<void> toggleLocked(int id) async {
try {
final locks = await repository.toggleLocked(id);
emit(LocksDisplayed().copyWith(List.from(locks)));
} on Exception {
emit(LockError());
}
}
}
My states:
abstract class LockState extends Equatable {
const LockState();
#override
List<Object> get props => [];
}
class LockInitial extends LockState {
#override
String toString() => 'LocksUninitialized';
}
class LockError extends LockState {
#override
String toString() => 'LockError';
}
class LocksDisplayed extends LockState {
final List<Lock> locks;
const LocksDisplayed([this.locks = const []]);
LocksDisplayed copyWith(locks) => LocksDisplayed(locks ?? this.locks);
#override
List<Object> get props => [locks];
#override
String toString() => 'LocksDisplayed { locks: $locks }';
}
My model:
class Lock extends Equatable {
Lock({this.id, this.name, this.locked, this.displayed});
final int id;
final String name;
final bool locked;
final bool displayed;
#override
String toString() =>
'Lock { id: $id name: $name locked: $locked displayed: $displayed }';
Lock copyWith({id, name, locked, displayed}) => Lock(
id: id ?? this.id,
name: name ?? this.name,
locked: locked ?? this.locked,
displayed: displayed ?? this.displayed);
#override
List<Object> get props => [id, name, locked, displayed];
}
My repositotory:
class LocksRepository {
List<Lock> locks = [];
Future<List<Lock>> fetchLocks() async {
// This is a temporary implementation
// In the future the data should be fetched from a provider
locks = [
new Lock(
id: 0,
name: 'Voordeur',
locked: false,
),
new Lock(
id: 1,
name: 'Achterdeur',
locked: false,
)
];
return locks;
}
Future<List<Lock>> toggleLocked(int id) async {
// This is a temporary implementation
// In the future a request to change a lock should be made and then the specific lock should be retrieved back and edited.
locks[id] = locks[id].copyWith(locked: !locks[id].locked);
return locks;
}
}
I am changing a state with the following trigger:
context.read<LockCubit>().toggleLocked(focusedIndex);
I am using BlocBuilder like this to build the state:
BlocBuilder<LockCubit, LockState>(builder: (context, state) {
print('State Changed');
if (state is LockInitial) {
return Text('lockInitial');
}
if (state is LocksDisplayed) {
return Swiper(
itemBuilder: (BuildContext context, int index) {
return Column(
children: [
Text(state.locks[index].name),
Text(state.locks[index].locked.toString())
],
);
},
onIndexChanged: onIndexChanged,
loop: true,
itemCount: state.locks.length);
}
if (state is LockError) {
return Text('lockError');
}
return Container();
});
All help would be very appreciated.
Can you check BlocProvider ? I got the same problem. If this bloc inside materialApp, you must pass BlocProvider.value not create in widget.
I am a bit confused, if this could work. But with a bloc you would use an event not a cubit (even though events are based on cubits).
So first of all I would use the standard pattern:
state
event
bloc with mapEventToState
Then, what I also do not see in your code, if you toggle your lock it would look like this in pseudo code
if (event is toggleLock) {
yield lockInProgress();
toggleLock();
yield locksDisplayed;
}
This way your state always changes from locksDisplayed to lockInProgress to locksDisplayed - just as you read in your link above