How Do I Properly Use Equatable To Recognise Deeply Nested Data Using Flutter_bloc 8.0.0+ - flutter

Introduction: I have been working on this problem for a while. The problem involves updating the state of deeply nested data with the Bloc 8.0.0+ paradigm. I am using a class that holds deeply nested data in it (in this minimum viable code: a ‘Collection’ that contains a name and children are meant to be a Show Series, and the Series has children that are meant to be Seasons, and Seasons have children that are meant to be Episodes).
The nested structure is something like this:
List<CollectionState>
|-- List<CollectionState>
| |-- List<CollectionState>
| | |-- List<CollectionState>
An important functionality of the code is that a child is added to the children list of the correct parent so it will display in the correct order of the hierarchy of parents to their children in the ListView; i.e. Collection has one Series (8768), and that Series has two Seasons (1817 and 7623), and when pressing on a Season, an Episode is added to its correct parent Season instead of being added to the bottom of the ListView. In this case, pressing on Season 1817 four times adds Episodes 2175, 2773, 5420 and 8826 under itself instead of adding to Season 7623.
Problem: As I understand it, a good practice while working with BLoC 8.0.0+ would be extending the CollectionState class with Equatable. The following code I provide works; however, it does not use this best practice. I want it to do so, but I am having problems which I will explain shortly. I have commented in the code of collection_state.dart with:
// 1) where Equatable should be extended
I have located in the code where the issue occurs in collection_bloc.dart when the CollectionState class is extended with Equatable (please note that the problem happens only with changing the code by extending CollectionState class with Equatable, which the code does not do). I have commented in the code with this comment:
//TODO: Here is the problem. This code does not work properly when I extend the CollectionState class to Equatable.
Surprisingly, there is little information online that I could find about using deeply nested data with BLoC 8.0.0+.
I am new to BLoC 8.0.0+ and even newer to Equatable (I’ve always used Provider up to this point) and I don’t understand why my code is not updating correctly when extending Equatable. I guess I am having an immutability issue because the update to the class with the AddInfo bloc event is not considered different to Equatable. I am at a loss to understand how to change my code to use the best practices with deeply nested data with BLoC.
Question:
How do I change my code to extend the CollectionState class with Equatable and still have it update my UI correctly?
Bearing in mind that I have a cursory understanding of Equatable, I would like to know more about the underlying root of the problem. Is it the case that the bloc event method is not producing a class different enough to Equatable, so it is not updating or is something entirely different happening here?
Note: When I extend Equatable and add the props for name, children, showType; and click on the collection, it adds Series as normal. But, when I click on Series, the app does not update to show the addition of the Seasons and Episodes. However, when I press Hot Reload, the app is refreshed with all the correct items. I believe that it is not comparing the nested objects, and I don't know how to allow it to compare them with my code.
pub spec.yaml
dependencies:
flutter:
sdk: flutter
flutter_bloc: ^8.1.1
equatable: ^2.0.5
main.dart
import 'package:deeply_nested_objects/bloc/add_to_collection_logic.dart';
import 'package:deeply_nested_objects/bloc/collection_bloc.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
#override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => CollectionBloc(),
child: const MaterialApp(
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
#override
Widget build(BuildContext context) {
return BlocBuilder<CollectionBloc, CollectionState>(
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: const Text('Deeply nested data and Bloc 8.0.0+'),
),
body: ListView.builder(
itemCount: state.getAllNodes(state).length,
itemBuilder: (context, index) {
var nodes = state.getAllNodes(state)[index];
Color textColor = getColor(nodes);
double distance = getPaddingDistance(nodes);
return Padding(
padding: EdgeInsets.only(left: distance),
child: ListTile(
onTap: () => addToCollectionLogic(nodes.showType, index,
nodes.children.length + 1, context),
leading: Card(
child: Text(nodes.name, style: TextStyle(color: textColor)),
),
),
);
},
),
);
},
);
}
double getPaddingDistance(CollectionState nodes) {
switch (nodes.showType) {
case ShowType.collection:
return 0;
case ShowType.series:
return 20;
case ShowType.season:
return 40;
case ShowType.episode:
return 60;
}
}
Color getColor(CollectionState nodes) {
switch (nodes.showType) {
case ShowType.collection:
return Colors.black;
case ShowType.series:
return Colors.blue;
case ShowType.season:
return Colors.green;
case ShowType.episode:
return Colors.red;
}
}
}
add_collection_logic.dart
import 'package:deeply_nested_objects/bloc/collection_bloc.dart';
import 'package:deeply_nested_objects/bloc/collection_event.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void addToCollectionLogic(
ShowType showType, int index, int count, BuildContext context) {
void passToBloc(String name, ShowType showType) =>
BlocProvider.of<CollectionBloc>(context).add(
AddInfo(
index: index,
child: CollectionState(name: name, showType: showType, children: []),
),
);
switch (showType) {
case ShowType.collection:
passToBloc('Series $count', ShowType.series);
break;
case ShowType.series:
passToBloc('Season $count', ShowType.season);
break;
case ShowType.season:
passToBloc('Episode $count', ShowType.episode);
break;
case ShowType.episode:
break;
}
}
collection_event.dart
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:equatable/equatable.dart';
abstract class CollectionEvents extends Equatable {
#override
List<Object> get props => [];
}
class AddInfo extends CollectionEvents {
AddInfo({required this.index, required this.child});
final int index;
final CollectionState child;
}
collection_bloc.dart
import 'package:deeply_nested_objects/bloc/collection_event.dart';
import 'package:deeply_nested_objects/bloc/collection_state.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CollectionBloc extends Bloc<CollectionEvents, CollectionState> {
CollectionBloc() : super(CollectionState.initial()) {
on<AddInfo>((event, emit) {
if (event.child.showType == ShowType.series) {
emit(state.copyWith(children: [...state.children, event.child]));
}
if (event.child.showType == ShowType.season ||
event.child.showType == ShowType.episode) {
//TODO: Here is the problem. This code does not work properly when I extend the CollectionState class to Equatable.
// get the list of all nodes
List<CollectionState> list = state.getAllNodes(state);
// find the parent node while still in the list
CollectionState parent = list[event.index];
// add the child to the parent
parent.children.add(event.child);
// update the state
emit(state.copyWith(children: [...state.children]));
}
});
}
}
collection_state.dart
enum ShowType { collection, series, season, episode }
// 1) where should be Equatable
class CollectionState {
const CollectionState({
required this.name,
required this.children,
required this.showType,
});
final String name;
final List<CollectionState> children;
final ShowType showType;
factory CollectionState.initial() {
return const CollectionState(
name: "Collection",
showType: ShowType.collection,
children: [],
);
}
List<CollectionState> getAllNodes(CollectionState node) {
// empty list to store the result
List<CollectionState> result = [];
// add the current node
result.add(node);
// add the children too
for (CollectionState child in node.children) {
// composite design pattern seek and find
result.addAll(getAllNodes(child));
}
return result;
}
CollectionState copyWith({
String? name,
List<CollectionState>? children,
ShowType? showType,
}) {
return CollectionState(
name: name ?? this.name,
children: children ?? this.children,
showType: showType ?? this.showType,
);
}
}

You have to assign properties in equatable that you want to check equality on.
Example:
class SomeClass extends Equatable {
SomeClass({required this.index});
final int index;
#override
List<Object> get props => [];
}
If I would check some SomeClass(index: 10) == SomeClass(index: 9) it would be true because I didn't say equatable what properties it should look for on == operator
If I update my code to
class SomeClass extends Equatable {
SomeClass({required this.index});
final int index;
#override
List<Object> get props => [index];
}
Now same check would be false, cause it is looking on index property

Related

Flutter bloc is not rebuilding in 7.2.0 version with Equatable

I created simple app to test bloc 7.2.0 and faced that BlocBuilder doesn't rebuild after first successful rebuild. On every other trigger bloc emits new state, but BlocBuilder ignores it.
Please note, if I remove extends Equatable and its override from both, state and event, then BlocBuilder rebuilds UI every time Button pressed. Flutter version 2.5.1
If Equatable is necessary, why it's not working with it? If Equatable isn't necessary, why it's been used in initial creation via VSCode extension.
My code:
bloc part
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
//bloc
class MainBloc extends Bloc<MainEvent, MainState> {
MainBloc() : super(MainInitial()) {
on<MainButtonPressedEvent>(_onMainButtonPressedEvent);
}
void _onMainButtonPressedEvent(
MainButtonPressedEvent event, Emitter<MainState> emit) {
emit(MainCalculatedState(event.inputText));
}
}
//states
abstract class MainState extends Equatable {
const MainState();
#override
List<Object> get props => [];
}
class MainInitial extends MainState {}
class MainCalculatedState extends MainState {
final String exportText;
const MainCalculatedState(this.exportText);
}
//events
abstract class MainEvent extends Equatable {
const MainEvent();
#override
List<Object> get props => [];
}
class MainButtonPressedEvent extends MainEvent {
final String inputText;
const MainButtonPressedEvent(this.inputText);
}
UI part
import 'package:bloc_test/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: BlocProvider(
create: (context) => MainBloc(),
child: SubWidget(),
),
),
);
}
}
class SubWidget extends StatelessWidget {
TextEditingController inputText = TextEditingController();
String? exportText;
#override
Widget build(BuildContext context) {
MainBloc mainBloc = BlocProvider.of<MainBloc>(context);
return BlocBuilder<MainBloc, MainState>(
builder: (context, state) {
if (state is MainCalculatedState) {
exportText = state.exportText;
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('${exportText ?? ''} data'),
SizedBox(
width: 200,
child: TextField(
controller: inputText,
),
),
ElevatedButton(
onPressed: () =>
mainBloc.add(MainButtonPressedEvent(inputText.text)),
child: const Text('Button')),
],
),
);
},
);
}
}
Equatable is used to make it easy for you to program, how and when states are the same (no update) and when they are different (update).
Your updates do not work because you are sending the same state repeatedly, but you did not tell the Equatable extension how to find out if they are different. So they are all the same.
So to make sure your program understands that some states of the same kind are indeed different and should cause an update, you need to make sure you mention what makes them different:
class MainCalculatedState extends MainState {
final String exportText;
const MainCalculatedState(this.exportText);
// this tells the Equatable base class to consider your text property
// when trying to figure out if two states are different.
// If the text is the same, the states are the same, so no update
// If the text is different, the states are different, so it will update
#override
List<Object> get props => [this.exportText];
}
If you remove Equatable altogether, two newly instanciated states are never equal, so that would solve your problem as well... except that at some point you will want them to be, and then you need to add it back in.
Your MainCalculatedState needs to override the props getter from Equatable and return the list of all properties which should be used to assess equality. In your case it should return [exportText].
Example:
class MainCalculatedState extends MainState {
final String exportText;
const MainCalculatedState(this.exportText);
#override
List<Object> get props => [exportText];
}

How can I iterate a collection (e.g. Map<String, supertype>) and render a separate widget for each subtype in the collection?

I have a list of objects that are all subtypes of Animal. I cast them all to supertype in the animal list to keep their references in one place. So far so good.
Now, when I render state information about each animal object I would like to use different widgets based on the subtype of each Animal object. When I cast from supertype to subtype, even when I specify the field is dynamic, I get the error.
Type 'type' is not a subtype of type '<subtype>'
It seems that the dart compiler uses the type information from the animalTiles map to enforce that all elements are of type Animal, even if the constructor that I pass the type into takes a dynamic named parameter, which I hoped would be looser.
// ANIMAL LIST
class AnimalList extends StatelessWidget {
final Map<String, Animal> animals;
#override
Widget build(BuildContext context) {
List<Widget> animalTiles = [];
animals.forEach(
(key, setting) => animalTiles.add(
AnimalTile(animal: animal),
),
);
return Column(
children: animalTiles,
);
}
}
// ANIMAL TILE
class AnimalTile extends StatelessWidget {
AnimalTile({
required animal,
}) {
_buildDescription(animal);
}
late dynamic animal;
late Widget description;
Widget _buildDescription(dynamic setting) {
if (animal.type == Animal.CAT) {
Cat cat = animal;
return CatWidget(cat: cat);
} else if (animal.type == Animal.DOG) {
Dog dog = animal;
return DogWidget(dog: dog);
} else if (animal.type == Animal.MOUSE) {
Mouse mouse = animal;
return MouseWidget(mouse: mouse);
} else {
return Container(child: Text(':('));
}
}
#override
Widget build(BuildContext context) {
return(
...
);
}
}
The Dart type system requires that I initialize final fields before the constructor body in an initialization list or assign directly to instance fields in the params list. If I call a helper method in the body of my constructor, that is not considered final so I would not like to use this for an immutable Stateless Widget.
This has to be a somewhat common pattern..I am trying to keep all subtypes in the same list and then use the subtype type to display different widgets. Why does this approach fail and what is a valid way to implement this pattern in Dart?
The Flutter Cookbook has an example of creating a list of items of different subtypes. However I find myself repeating data fields that I was hoping would be common to the base class. Because, if I use extends in place of implements (i.e. inheritance over interface) for the abstract Animal class, I get an Exception indicating that Dart is looking at the method of the base class, not the overridden method in the subclass. This is not the best solution for keeping your code DRY, but it works.
Unfortunately, this means that I do not have a solution that enables me to keep data fields used by my many animals within a single class, but I am posting this to demonstrate a pattern that works.
UPDATE:
You may also be able to reuse data-fields across subtypes while ensuring that the methods called are those overridden by the subtype using both extends (inheritance) and implements (interface).
Here is my solution, please outdo me:
// Base Class
abstract class AnimalBase {
AnimalBase({
required this.height,
required this.weight,
required this.name,
required this.genus,
required this.species,
});
final int height;
final double weight;
final String name;
final String genus;
final String species;
static const CAT = 0;
static const DOG = 1;
static const MOUSE = 2;
static AnimalFactory(Map<String, dynamic> json, int type) {
// IN MY CASE I PASS THE TYPE TO A FACTORY
switch (type) {
case CAT:
return Cat.fromJson(json);
case DOG:
return Dog.fromJson(json);
case MOUSE:
return Mouse.fromJson(json);
default:
return null;
}
}
}
// Interface Class
abstract class Animal; {
Widget buildTile();
Widget description();
}
// Example SubType
class Dog extends AnimalBase with Animal {
Dog({
required height,
required weight,
required name,
required genus,
required species,
}) : super(height: height, weight: weight, name: name, genus: genus, species: species);
#override
Dog.fromJson(Map<String, dynamic> json)
throw new UnimplementedError();
}
Map<String, dynamic> toJson() {
throw new UnimplementedError();
}
#override
Widget buildTile() {
return DogTile(dog: this);
}
#override
Widget description() {
return DogDescription(dog: this);
}
}
class AnimalList extends StatelessWidget {
AnimalList({
Key? key,
required this.animals,
}) : super(key: key);
final Map<String, Animal> animals;
#override
Widget build(BuildContext context) {
List<Widget> animalTiles = [];
animals.forEach(
(key, animal) => animalTiles.add(
AnimalTile(animal: animal),
),
);
return CustomScrollView(
shrinkWrap: true,
slivers: [
SliverToBoxAdapter(
child: Column(
children: ...animalTiles.map(animal => animal.buildTile()).toList(),
),
),
SliverFillRemaining(
hasScrollBody: false,
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Divider(
color: Colors.black54,
height: 18,
thickness: 4,
indent: 20,
endIndent: 20,
),
],
),
),
],
);
}
}
I wish I had a solution that only required one keyword (extends or implements) but this is the best I have :)

BlocBuilder not updating after change

I have a bloc to manage all the quotations in the application. The quotation class, bloc, and events are given below:
I have a form in which on selecting the text field, I show a list view to the user, and the value of the selected list view is assigned to the bloc and displayed in the text field.
Everything works fine but when I assign the value to the bloc variable and return it back to the form the text field value does update BUT ONLY FOR SINGLE TIME. If I do select some other list option for the same or another field the field value doesn't update.
CAN ANYONE SUGGEST A FIX?
I have a custom textField created as shown below and I'm calling this inside a bloc builder:
BlocBuilder<QuoteBloc, QuoteState>(builder: (context, state) {
if (state is QuoteInitialized) {
return Column(
children: [
BookingFormField(
labelText: "Flying From",
onTap: () => Navigator.push(
context,
AirportCityPlaceSelection.route(
'tq-fb-flight-from',
),
),
controller: TextEditingController(
text: BlocProvider.of<QuoteBloc>(context)
.quote
.flight
.flightFrom,
),
),
BookingFormField(
labelText: "Flying To",
onTap: () {
Navigator.push(
context,
AirportCityPlaceSelection.route(
'tq-fb-flight-to',
),
);
},
controller: TextEditingController(
text: BlocProvider.of<QuoteBloc>(context)
.quote
.flight
.flightTo,
),
),
],
);
}
}),
class BookingFormField extends StatelessWidget {
final Function onTap;
final TextEditingController controller;
final String labelText;
BookingFormField({
#required this.onTap,
#required this.controller,
#required this.labelText,
});
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(
top: 10.0,
bottom: 10.0,
),
child: TextField(
controller: controller,
readOnly: true,
onTap: () => onTap(),
style: Theme.of(context).textTheme.bodyText2.copyWith(
fontSize: 13.0,
fontWeight: FontWeight.w600,
color: Theme.of(context).primaryColor,
),
);
}
}
And this is how I'm updating the value in the list view which is a new screen:
BlocProvider.of<QuoteBloc>(context).quote.flight.flightFrom = value;
BlocProvider.of<QuoteBloc>(context).add(QuoteUpdated());
Navigator.pop(context);
Quote Class:-
part 'flight.dart';
part 'car.dart';
part 'cruise.dart';
part 'hotel.dart';
part 'visa.dart';
part 'insurance.dart';
part 'transfer.dart';
class Quote {
String name;
String contactNumber;
String email;
Flight flight;
Car car;
Hotel hotel;
Cruise cruise;
Transfer transfer;
Visa visa;
Insurance insurance;
// Constructors & other functions
}
The events related to the quote bloc are:
abstract class QuoteEvent extends Equatable {
List<Object> get props => [];
}
class QuoteUpdated extends QuoteEvent {
List<Object> get props => [];
}
The quote State is
abstract class QuoteState extends Equatable {
List<Object> get props => [];
}
class QuoteInitialized extends QuoteState {
final Quote quote;
QuoteInitialized({
#required this.quote,
});
List<Object> get props => [this.quote];
}
class QuoteSubmissionInProgress extends QuoteState {}
class QuoteSubmissionSuccessful extends QuoteState {}
class QuoteSubmissionFailed extends QuoteState {}
Quote Bloc:
class QuoteBloc extends Bloc<QuoteEvent, QuoteState> {
final Quote quote;
QuoteBloc(Quote quote)
: assert(quote != null),
this.quote = quote,
super(QuoteInitialized(quote: quote));
#override
Stream<QuoteState> mapEventToState(QuoteEvent event) async* {
if (event is QuoteUpdated) {
yield QuoteInitialized(quote: this.quote);
}
}
}
Don't update state in UI LAYER (send event to bloc)
Try to remove equatable in QuoteState or Add Equatable to Quote class
A guess is that the state is considered to be the same, meaning that the following times you expect updated fields you actually didn't get a new state. Have you verified that you get a new yielded state in the BlocBuilder?
My guess is based on two things. Firstly, that symptom could manifest in that way. Secondly I don't see methods in the Quote class that allow for equals comparison (maybe you have it where you commented out code).
I had a similar problem which gave me a headache. I was using a cubit and it won't display a progress bar because the loading state was not set. Since bloc extends cubit you might have the same problem. I had to put a future.delayed before emitting the SearchLoading() state. After this change, the state was set and the progress bar was shown. I had this problem in the debug mode of an Android app as well as in the release build.
class SearchCubit extends Cubit<SearchState> {
final ClubRepository _clubRepository = ClubRepository();
final log = getLogger("SearchCubit");
SearchCubit() : super(SearchInitial());
Future<void> getClubs() async {
try {
log.d("Fetch clubs");
await Future.delayed(Duration(microseconds: 1));
emit(SearchLoading());
final List<Club> clubs = await _clubRepository.fetch();
await Future.delayed(Duration(seconds: 2));
emit(SearchLoaded(clubs));
} catch (err, stacktrace) {
emit(SearchError("Retrieving data from API failed!"));
}
}
}
I'm guessing this is happening because your Quote class does not extend Equatable. Please refer to the FAQs for more information 👍

Flutter BLoC can't update my list of boolean

So, I tried to learn flutter especially in BLoC method and I made a simple ToggleButtons with BLoC. Here it looks like
ToggleUI.dart
class Flutter501 extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter 50 With Bloc Package',
home: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
BlocProvider<ToggleBloc>(
builder: (context) => ToggleBloc(maxToggles: 4),
child: MyToggle(),
)
],
),
),
),
);
}
}
class MyToggle extends StatelessWidget {
const MyToggle({
Key key,
}) : super(key: key);
#override
Widget build(BuildContext context) {
ToggleBloc bloc = BlocProvider.of<ToggleBloc>(context);
return BlocBuilder<ToggleBloc, List<bool>>(
bloc: bloc,
builder: (context, state) {
return ToggleButtons(
children: [
Icon(Icons.arrow_back),
Icon(Icons.arrow_upward),
Icon(Icons.arrow_forward),
Icon(Icons.arrow_downward),
],
onPressed: (idx) {
bloc.dispatch(ToggleTap(index: idx));
},
isSelected: state,
);
},
);
}
}
ToogleBloc.dart
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/cupertino.dart';
abstract class ToggleEvent extends Equatable {
const ToggleEvent();
}
class ToggleTap extends ToggleEvent {
final int index;
ToggleTap({this.index});
#override
// TODO: implement props
List<Object> get props => [];
}
class ToggleBloc extends Bloc<ToggleEvent, List<bool>> {
final List<bool> toggles = [];
ToggleBloc({
#required int maxToggles,
}) {
for (int i = 0; i < maxToggles; i++) {
this.toggles.add(false);
}
}
#override
// TODO: implement initialState
List<bool> get initialState => this.toggles;
#override
Stream<List<bool>> mapEventToState(ToggleEvent event) async* {
// TODO: implement mapEventToState
if (event is ToggleTap) {
this.toggles[event.index] = !this.toggles[event.index];
}
yield this.toggles;
}
}
The problem came when I tried to Tap/Press one of the buttons, but it doesn't want to change into the active button. But it works whenever I tried to press the "Hot Reload". It likes I have to make a setState whenever the button pressed.
The BlocBuilder.builder method is only executed if the State changes. So in your case the State is a List<bool> of which you only change a specific index and yield the same object. Because of this, BlocBuilder can't determine if the List changed and therefore doesn't trigger a rebuild of the UI.
See https://github.com/felangel/bloc/blob/master/docs/faqs.md for the explanation in the flutter_bloc docs:
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.
Solution
In your ToggleBloc, change the List like this, so it creates a completely new List object:
#override
Stream<List<bool>> mapEventToState(ToggleEvent event) async* {
// TODO: implement mapEventToState
if (event is ToggleTap) {
this.toggles[event.index] = !this.toggles[event.index];
this.toggles = List.from(this.toggles);
}
yield this.toggles;
}
Also, make sure to set the props for your event, although it won't really matter for this specific question.
BlocBuilder will ignore the update if a new state was equal to the old state. When comparing two lists in Dart language, if they are the same instance, they are equal, otherwise, they are not equal.
So, in your case, you would have to create a new instance of list for every state change, or define a state object and send your list as property of it.
Here is how you would create new list instance for every state:
if (event is ToggleTap) {
this.toggles[event.index] = !this.toggles[event.index];
}
yield List.from(this.toggles);
You can read more about bloc library and equality here:
https://bloclibrary.dev/#/faqs?id=when-to-use-equatable

Why we should use Equatable in Flutter Bloc?

I understand that Equatable helps to compare two instances of object without doing it manually.
But where exactly I can use it in Flutter Bloc?
Here is the example of usage Equatable:
Where it could be useful?
abstract class TodosState extends Equatable {
const TodosState();
#override
List<Object> get props => [];
}
class TodosLoadInProgress extends TodosState {}
class TodosLoadSuccess extends TodosState {
final List<Todo> todos;
const TodosLoadSuccess([this.todos = const []]);
#override
List<Object> get props => [todos];
#override
String toString() => 'TodosLoadSuccess { todos: $todos }';
}
class TodosLoadFailure extends TodosState {}
Object and data comparison is always hard to do when it comes to stream as we need to decide state updation based on it.
we required Equatable as 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 TodosState extends Equatable {}
So, that means TodosState will not make duplicate calls and will not going to rebuild the widget if the same state occurs.
Let's see props usage in Equatable and what makes it special
Define State without props:
class TodosLoadSuccess extends TodosState {}
Define State with props:
props declared when we want State to be compared against the values which declared inside props List
class TodosLoadSuccess extends TodosState {
final String name;
final List<Todo> todos;
const TodosLoadSuccess([this.name, this.todos = const []]);
#override
List<Object> get props => [name, todos];
}
If we remove the name from the list and keep a list like [this.todos], then State will only consider the todos field, avoiding the name 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. For example, let's look at the below example here TodosState will build a widget only once, which will avoid the second call as it is duplicated.
#override
Stream<TodosState> mapEventToState(MyEvent event) async* {
final List<Todo> todos = [Todo(), Todo()];
yield TodosLoadSuccess(todos);
yield TodosLoadSuccess(todos); // This will be avoided
}
Detail Blog: https://medium.com/flutterworld/flutter-equatable-its-use-inside-bloc-7d14f3b5479b
I think it is useful for comparing what state is in BlocBuilder.
Below code is a good example of using Equatable.
if(state is [Some State])
#override
Widget build(BuildContext context) {
return BlocBuilder<SongsSearchBloc, SongsSearchState>
bloc: BlocProvider.of(context),
builder: (BuildContext context, SongsSearchState state) {
if (state is SearchStateLoading) {
return CircularProgressIndicator();
}
if (state is SearchStateError) {
return Text(state.error);
}
if (state is SearchStateSuccess) {
return state.songs.isEmpty
? Text(S.EMPTY_LIST.tr())
: Expanded(
child: _SongsSearchResults(
songsList: state.songs,
),
);
} else {
return Text(S.ENTER_SONG_TITLE.tr());
}
},
);
}