0
votes

I'm developing a web application using flutter_bloc library for state management. In my app I have to keep track of the state of a form (AreaForm widget). In this form I have to manage a list of objects: I must be able to add or remove an object to the list.

This is the code for the AreaFormState:

part of 'area_form_bloc.dart';

class AreaFormState extends Equatable {
  final Provincia provincia;
  final Comune comune;
  final String denominazione;
  final List<TipoUdo> listaTipoUdo;
  final List<Specialita> listaSpecialita;
  final bool isDisciplineChecked;
  final bool isBrancheChecked;
  final String indirizzo;

  const AreaFormState({
    @required this.provincia,
    @required this.comune,
    @required this.denominazione,
    @required this.listaTipoUdo,
    @required this.listaSpecialita,
    @required this.isDisciplineChecked,
    @required this.isBrancheChecked,
    @required this.indirizzo,
  });

  factory AreaFormState.empty() {
    return AreaFormState(
      provincia: null,
      comune: null,
      denominazione: null,
      listaTipoUdo: <TipoUdo>[],
      listaSpecialita: <Specialita>[],
      isDisciplineChecked: false,
      isBrancheChecked: false,
      indirizzo: null,
    );
  }

  AreaFormState update({
    Provincia provincia,
    Comune comune,
    String denominazione,
    List<TipoUdo> listaTipoUdo,
    List<Specialita> listaSpecialita,
    bool isDisciplineChecked,
    bool isBrancheChecked,
    String indirizzo,
  }) {
    return copyWith(
      provincia: provincia,
      comune: comune,
      denominazione: denominazione,
      listaTipoUdo: listaTipoUdo,
      listaSpecialita: listaSpecialita,
      isDisciplineChecked: isDisciplineChecked,
      isBrancheChecked: isBrancheChecked,
      indirizzo: indirizzo,
    );
  }

  AreaFormState copyWith({
    Provincia provincia,
    Comune comune,
    String denominazione,
    List<TipoUdo> listaTipoUdo,
    List<Specialita> listaSpecialita,
    bool isDisciplineChecked,
    bool isBrancheChecked,
    String indirizzo,
  }) {
    return AreaFormState(
      provincia: provincia ?? this.provincia,
      comune: comune ?? this.comune,
      denominazione: denominazione ?? this.denominazione,
      listaTipoUdo: listaTipoUdo ?? this.listaTipoUdo,
      listaSpecialita: listaSpecialita ?? this.listaSpecialita,
      isDisciplineChecked: isDisciplineChecked ?? this.isDisciplineChecked,
      isBrancheChecked: isBrancheChecked ?? this.isBrancheChecked,
      indirizzo: indirizzo ?? this.indirizzo,
    );
  }

  @override
  List<Object> get props => [
        provincia,
        comune,
        denominazione,
        listaTipoUdo,
        listaSpecialita,
        isDisciplineChecked,
        isBrancheChecked,
        indirizzo,
      ];

  @override
  String toString() {
    return '''
    AreaFormState {
      provincia: $provincia,
      comune: $comune,
      denominazione: $denominazione,
      listaTipoUdo: $listaTipoUdo,
      listaSpecialita: $listaSpecialita,
      isDisciplineChecked: $isDisciplineChecked,
      isBrancheChecked: $isBrancheChecked,
      indirizzo: $indirizzo,
    }''';
  }
}

This is the code for the AreaFormEvent (I have reported only the two events of interest):

part of 'area_form_bloc.dart';

abstract class AreaFormEvent extends Equatable {
  const AreaFormEvent();

  @override
  List<Object> get props => [];
}

...

class SpecialitaAdded extends AreaFormEvent {
  final Specialita specialita;

  const SpecialitaAdded({@required this.specialita});

  @override
  List<Object> get props => [specialita];

  @override
  String toString() => 'SpecialitaAdded { specialita: $specialita }';
}

class SpecialitaRemoved extends AreaFormEvent {
  final Specialita specialita;

  const SpecialitaRemoved({@required this.specialita});

  @override
  List<Object> get props => [specialita];

  @override
  String toString() => 'SpecialitaRemoved { specialita: $specialita }';
}

...

And finally this is the code for AreaFormBloc:

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

import '../../../data/comune.dart';
import '../../../data/provincia.dart';
import '../../../data/specialita.dart';
import '../../../data/tipo_udo.dart';
import '../../repositories/area/area_repository.dart';

part 'area_form_event.dart';
part 'area_form_state.dart';

class AreaFormBloc extends Bloc<AreaFormEvent, AreaFormState> {
  final AreaRepository areaRepository;

  AreaFormBloc({@required this.areaRepository})
      : assert(areaRepository != null);

  @override
  AreaFormState get initialState => AreaFormState.empty();

  @override
  Stream<AreaFormState> mapEventToState(
    AreaFormEvent event,
  ) async* {
    if (event is ProvinciaSelected) {
      yield* _mapProvinciaSelectedToState(event.provincia);
    } else if (event is ComuneSelected) {
      yield* _mapComuneSelectedToState(event.comune);
    } else if (event is DenominazioneChanged) {
      yield* _mapDenominazioneChangedToState(event.denominazione);
    } else if (event is TipoUdoAdded) {
      yield* _mapTipoUdoAddedToState(event.tipoUdo);
    } else if (event is TipoUdoRemoved) {
      yield* _mapTipoUdoRemovedToState(event.tipoUdo);
    } else if (event is SpecialitaAdded) {
      yield* _mapSpecialitaAddedToState(event.specialita);
    } else if (event is SpecialitaRemoved) {
      yield* _mapSpecialitaRemovedToState(event.specialita);
    } else if (event is DisciplineChanged) {
      yield* _mapDisciplineChangedToState();
    } else if (event is BrancheChanged) {
      yield* _mapBrancheChangedToState();
    } else if (event is IndirizzoChanged) {
      yield* _mapIndirizzoChangedToState(event.indirizzo);
    } else if (event is NearestUdoIconPressed) {
      yield* _mapNearestUdoIconPressedToState();
    } else if (event is PulisciPressed) {
      yield* _mapPulisciPressedToState();
    } else if (event is CercaPressed) {
      yield* _mapCercaPressedToState();
    }
  }

  ...

  Stream<AreaFormState> _mapSpecialitaAddedToState(
    Specialita specialita,
  ) async* {

    yield state.update(listaSpecialita: state.listaSpecialita..add(specialita));
  }

  Stream<AreaFormState> _mapSpecialitaRemovedToState(
    Specialita specialita,
  ) async* {
    yield state.update(
        listaSpecialita: state.listaSpecialita..remove(specialita));
  }

  ...

}

In this way, when the event SpecialitaAdded is added to the AreaFormBloc, the bloc should produce a Transition to a new AreaFormState which should have a listaSpecialita equal to the one before with the addition of the new Specialita object just added.

Unfortunately no transition at all is triggered! But the really strange thing is that if I add the element like this instead:

  Stream<AreaFormState> _mapSpecialitaAddedToState(
    Specialita specialita,
  ) async* {
    yield state.update(listaSpecialita: state.listaSpecialita + [specialita]);
  }

then the transition is triggered.

Unfortunately I can't keep this solution because I don't know how to manage the removal of an element.

I think the problem is that I use the Equatable package for equality comparison but I really don't understand where I'm wrong.

2

2 Answers

2
votes

I solved the problem using Dart's spread operator:

  Stream<AreaFormState> _mapSpecialitaAddedToState(
    Specialita specialita,
  ) async* {
    yield state.update(listaSpecialita: [...state.listaSpecialita]..add(specialita));
  }

  Stream<AreaFormState> _mapSpecialitaRemovedToState(
    Specialita specialita,
  ) async* {
    yield state.update(
        listaSpecialita: [...state.listaSpecialita]..remove(specialita));
  }
1
votes

With Equatable you have to have immutable fields (your list is modifiable).

Something like this happened I think:

final someList = <int>[1, 2, 3]; // this is done when creating state
final anotherList = someList; // this is done with update, you just pass the reference
someList.add(4);
print(anotherList == someList); // true

So equatable, what is does it checks if the passed props are equal or not, in this case are equal.

The solution:

Stream<AreaFormState> _mapSpecialitaAddedToState(
  Specialita specialita,
) async* {
  // creating a new copy of the list
  yield state.update(
    listaSpecialita: List<Specialita>.of(
      state.listaSpecialita..add(specialita),
    ),
  );
}

This gives the List a new reference with the same values. Which will make the internal == check false.

Bloc doesn't emit a new state unless they are not equal.

Would advise you to use freezed package instead, it will save you the hussle of copyWith.