Cubit not rebuilding when emiting a new state - flutter

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

Related

Flutter bloc does not fetch data from API

I have some data that I want to fetch when the page loads up.
Below is the code for fetching the data in the screen
class _HighSchoolScreenState extends State<HighSchoolScreen> {
late PagingController<int, HighSchool> _pagingController;
#override
void initState() {
_pagingController = context.read<HighSchoolsBloc>().pageController;
_pagingController.addPageRequestListener(
(pageKey) {
context.read<HighSchoolsBloc>().add(
FetchHighSchools(page: pageKey, category: widget.category!),
);
},
);
super.initState();
}
I am using the infinite_scroll_pagination package to lazy load the data in the UI
Widget build(BuildContext context) => PagedListView<int, HighSchool>(
addAutomaticKeepAlives: false,
shrinkWrap: true,
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<HighSchool>(
animateTransitions: true,
newPageProgressIndicatorBuilder: (context) => const CircularProgressIndicator.adaptive(),
firstPageProgressIndicatorBuilder: (context) => const CircularProgressIndicator.adaptive(),
itemBuilder: (context, item, index) => SchoolsContent(
item: item,
theme: theme,
isIos: isIos,
),
),
),
Below is also my bloc for the data
class HighSchoolsBloc extends Bloc<HighSchoolsEvent, HighSchoolsState> {
final String token = Hive.box('user').get(kToken);
bool hasNextPage = true;
late HighSchoolRepo _highSchoolRepo;
final PagingController<int, HighSchool> pageController =
PagingController(firstPageKey: 0);
Future<void> _fetchPage(int pageKey, FetchHighSchools event) async {
try {
final results = await _highSchoolRepo.get(
page: event.page,
category: event.category,
token: token,
);
hasNextPage = results['hasNextPage'];
final List<HighSchool> newItems = results['schools'];
if (!hasNextPage) {
pageController.appendLastPage(newItems);
} else {
final nextPageKey = pageKey + 1;
pageController.appendPage(newItems, nextPageKey);
}
} catch (error) {
pageController.error = 'error';
}
}
HighSchoolsBloc(this._highSchoolRepo) : super(InitailState()) {
on<FetchHighSchools>((event, emit) {
print('new ${event.category}');
_fetchPage(event.page, event);
});
}
}
So the real issue is whenever visit the screen, the data fetches correctly and shows on the screen(UI) but when I leave the screen and press on another category, it should fetch data based on the different category now but it is not event fetching anything again. it just shows the same data that was fetched previously
Below is the states for my bloc
#immutable
abstract class HighSchoolsState extends Equatable {
#override
List<Object?> get props => [];
}
// ignore_for_file: public_member_api_docs, sort_constructors_first
class InitailState extends HighSchoolsState {}
class HighSchoolFetchError extends HighSchoolsState {
late final String error;
HighSchoolFetchError(this.error);
#override
List<Object?> get props => [error];
}
PLEASE NOT THAT LOADING AND ERROR ARE HANDLED BY THE PACKAGE SO THERE'S NO NEED TO MAKE IT'S RELATIVE STATES
ALSO, ONE MORE ERROR I AM FACING IS SOMETIMES, WHEN I SCROLL THROUGH THE DATA GIVEN IT GIVES ME AN ERROR OF
This widget has been unmounted, so the State no longer has a context (and should be considered defunct). // It appears on line 32 which is where the initstate it.

Best practice on how to write/update data from a Flutter provider

I'm fairly new to Flutter providers. I use Riverpod.
I have a Future provider that provide some data from a JSON file - in the future it will be from a API response.
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/pokemon.dart';
final pokemonProvider = FutureProvider<List<Pokemon>>((ref) async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
return List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
});
I subscribe to with ref.watch in ConsumerState widgets, e.g.:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final AsyncValue<List<Pokemon>> pokemons =
ref.watch(pokemonProvider);
return pokemons.when(
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
data: (pokemons) {
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
},
);
}
}
But in that case, what is the best practice to write/update data to the JSON file/API?
It seems providers are used for reading/providing data, not updating it, so I'm confused.
Should the same provider pokemonProvider be used for that? If yes, what is the FutureProvider method that should be used and how to call it? If not, what is the best practice?
I am new to riverpod too but I'll try to explain the approach we took.
The examples with FutureProviders calling to apis are a little bit misleading for me, because the provider only offers the content for a single api call, not access to the entire api.
To solve that, we found the Repository Pattern to be very useful. We use the provider to export a class containing the complete api (or a mock one for test purposes), and we control the state (a different object containing the different situations) to manage the responses and updates.
Your example would be something like this:
First we define our state object:
enum PokemonListStatus { none, error, loaded }
class PokemonListState {
final String? error;
final List<Pokemon> pokemons;
final PokemonListStatus status;
const PokemonListState.loaded(this.pokemons)
: error = null,
status = PokemonListStatus.loaded,
super();
const PokemonListState.error(this.error)
: pokemons = const [],
status = PokemonListStatus.error,
super();
const PokemonListState.initial()
: pokemons = const [],
error = null,
status = PokemonListStatus.none,
super();
}
Now our provider and repository class (abstract is optional, but let's take that approach so you can keep the example for testing):
final pokemonRepositoryProvider =
StateNotifierProvider<PokemonRepository, PokemonListState>((ref) {
final pokemonRepository = JsonPokemonRepository(); // Or ApiRepository
pokemonRepository.getAllPokemon();
return pokemonRepository;
});
///
/// Define abstract class. Useful for testing
///
abstract class PokemonRepository extends StateNotifier<PokemonListState> {
PokemonRepository()
: super(const PokemonListState.initial());
Future<void> getAllPokemon();
Future<void> addPokemon(Pokemon pk);
}
And the implementation for each repository:
///
/// Class to manage pokemon api
///
class ApiPokemonRepository extends PokemonRepository {
ApiPokemonRepository() : super();
Future<void> getAllPokemon() async {
try {
// ... calls to API for retrieving pokemon
// updates cached list with recently obtained data and call watchers.
state = PokemonListState.loaded( ... );
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
Future<void> addPokemon(Pokemon pk) async {
try {
// ... calls to API for adding pokemon
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
} catch (e) {
state = PokemonListState.error(e.toString());
}
}
}
and
///
/// Class to manage pokemon local json
///
class JsonPokemonRepository extends PokemonRepository {
JsonPokemonRepository() : super();
Future<void> getAllPokemon() async {
var response =
await rootBundle.loadString('assets/mock_data/pokemons.json');
List<dynamic> data = jsonDecode(response);
// updates cached list with recently obtained data and call watchers.
final pokemons = List<Pokemon>.from(data.map((i) => Pokemon.fromMap(i)));
state = PokemonListState.loaded(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {
// ... and write json to disk for example
// updates cached list and calls providers watching.
state = PokemonListState.loaded([...state.pokemons, pk]);
}
}
Then in build, your widget with a few changes:
class PokemonsPage extends ConsumerStatefulWidget {
const PokemonsPage({Key? key}) : super(key: key);
#override
ConsumerState<PokemonsPage> createState() => _PokemonsPageState();
}
class _PokemonsPageState extends ConsumerState<PokemonsPage> {
#override
Widget build(BuildContext context) {
final statePokemons =
ref.watch(pokemonRepositoryProvider);
if (statePokemons.status == PokemonListStatus.error) {
return Text('Error: ${statePokemons.error}');
} else if (statePokemons.status == PokemonListStatus.none) {
return const CircularProgressIndicator();
} else {
final pokemons = statePokemons.pokemons;
return Material(
child: ListView.builder(
itemCount: pokemons.length,
itemBuilder: (context, index) {
Pokemon pokemon = pokemons[index];
return ListTile(
title: Text(pokemon.name),
);
},
));
}
}
}
Not sure if this is the best approach but it is working for us so far.
you can try it like this:
class Pokemon {
Pokemon(this.name);
final String name;
}
final pokemonProvider =
StateNotifierProvider<PokemonRepository, AsyncValue<List<Pokemon>>>(
(ref) => PokemonRepository(ref.read));
class PokemonRepository extends StateNotifier<AsyncValue<List<Pokemon>>> {
PokemonRepository(this._reader) : super(const AsyncValue.loading()) {
_init();
}
final Reader _reader;
Future<void> _init() async {
final List<Pokemon> pokemons;
try {
pokemons = await getApiPokemons();
} catch (e, s) {
state = AsyncValue.error(e, stackTrace: s);
return;
}
state = AsyncValue.data(pokemons);
}
Future<void> getAllPokemon() async {
state = const AsyncValue.loading();
/// do something...
state = AsyncValue.data(pokemons);
}
Future<void> addPokemon(Pokemon pk) async {}
Future<void> updatePokemon(Pokemon pk) async {}
Future<void> deletePokemon(Pokemon pk) async {}
}

Why emit.forEach(..) doesn't persist the state of the bloc in this case?

Simplified example using a Todo App approach where data is submitted from different pages and bloc reacts to it subscribing to a stream.
View, I have four pages.
Page A: Shows a ListView of Todos.
Page B-1: Shows a Form to update the overview data related to a Todo.
Page B-2: Shows a ListView of Actions that has a Todo.
Page C: Shows a Form to update the data related to an Action.
Logic, I have four blocs. CollectionBloc which subscribes to a stream of data using Hive and it is supposed to emit states every time there is an update in the repository. Also, EditTodoBloc and ActionBloc which submit data to the same repository. TodoBloc is for managing a Todo in general.
EditTodoBloc
----------> Page B-1
Page A ----------> | ActionBloc
CollectionBloc ----------> Page B-2 ----------> Page C
TodoBloc
Models
#HiveType(typeId: 0)
class Action extends Equatable {
Action({this.id, this.name});
#HiveField(0)
String id;
#HiveField(1)
String name;
#override
List<Object?> get props => [id, name];
}
#HiveType(typeId: 1)
class Todo extends Equatable {
Todo({this.id, this.actions});
#HiveField(0)
String id;
#HiveField(1)
String name;
#HiveField(2)
List<Action> actions;
...
#override
List<Object?> get props => [id, name, actions];
}
Database / Repository
class HiveDatabase {
late Box<List<Todo>> todos;
...
Stream<List<Todo>> watchTodos() {
return todos
.watch()
.map((event) => todos.values.toList())
.startWith(todos.values.toList());
}
Future<void> saveTodo(Todo todo) async {
await todos.put(todo.id, todo);
}
Future<void> saveAction(Todo todo, Action action) async {
todo.actions.add(action);
await todos.put(todo.id, todo);
}
}
Blocs
EditTodoBloc:
class EditTodoBloc extends Bloc<EditTodoEvent, EditTodoState> {
EditTodoBloc({
required TodosRepository todosRepository,
required Todo? todo,
}) : _todosRepository = todosRepository, super(EditTodoState(todo)) {
on<TodoSubmitted>(_onTodoSubmitted);
}
...
Future<void> _onTodoSubmitted(
TodoSubmitted event,
Emitter<EditTodoState> emit,
) async {
emit(state.copyWith(status: EditTodoStaus.loading));
try {
await _todosRepository.saveTodo(state.todo!);
emit(state.copyWith(status: EditTodoStaus.success));
} catch (e) {}
}
}
class EditTodoState extends Equatable {
final EditTodoStatus status;
final Todo? todo;
...
}
TodoBloc:
class TodoBloc extends Bloc<TodoEvent, TodoState> {
TodoBloc({
required TodosRepository todosRepository,
required Todo todo,
}) : _todosRepository = todosRepository, super(TodoState(todo)) {
...
}
}
class TodoState extends Equatable {
final TodoStatus status;
final Todo todo,
final List<Action> actions;
...
}
Action Bloc:
class ActionBloc extends Bloc<ActionEvent, ActionState> {
ActionBloc({
required TodosRepository todosRepository,
required Todo todo,
required Action? action,
}) : _todosRepository = todosRepository, super(ActionState(todo, action)) {
on<ActionSubmitted>(_onActionSubmitted);
}
...
Future<void> _onActionSubmitted(
ActionSubmitted event,
Emitter<ActionState> emit,
) async {
emit(state.copyWith(status: ActionStatus.loading));
try {
await _todosRepository.saveAction(todo, state.action!);
emit(state.copyWith(status: ActionStatus.success));
} catch(e) {}
}
}
class ActionState extends Equatable {
final ActionStatus status;
final Todo todo,
final Action? action;
...
}
And the problem is here.
CollectionBloc can't persist the state of the bloc when I submit data by adding an event from Page C (ActionBloc). Unlike when sending data from Page B-1 (EditTodoBloc) which works successfully.
CollectionBloc:
class CollectionBloc extends Bloc<CollectionEvent, CollectionState> {
CollectionBloc({
required TodosRepository todosRepository,
}) : super(CollectionState()) {
on<CollectionRequested>(_onCollectionRequested);
}
...
Future<void> _onCollectionRequested(
CollectionRequested event,
Emitter<CollectionState> emit,
) async {
emit(state.copyWith(status: TodoStatus.loading));
await emit.forEach<List<Todo>>(
_todosRepository.watchTodos(),
onData: (todos) {
print('newTodos: ${todos}');
print('oldTodos: ${state.todos}');
// Why oldTodos shows the same modified todo list (with its actions)
// as the one returned from onData
// Page-B2 does not update coming back from Page-C unless I pop up to
// Page-A and then push to Page-B2.
return state.copyWith(status: CollectionStatus.success, todos: todos);
},
onError: (_, __) => state.copyWith(status: CollectionStatus.failure),
);
}
}
class CollectionState extends Equatable {
final CollectionStatus status;
final List<Todo> todos;
...
}
Page A:
class PageA extends StatelessWidget {
const PageA({Key? key}) : super(key: key);
...
#override
Widget build(BuildContext context) {
final todos = context.watch<CollectionBloc>().state.todos;
return ListView(
children: [
for (final todo in todos) ...[
ListTile(
title: Text(todo.name),
onTap: () {
Navigator.of(context).push(
PageB2.route(todo),
);
},
),
],
],
);
}
}
All in all, I would like to be able to show the list of actions updated when popping back from submitting the form in Page C to Page B-2.
class PageB2 extends StatelessWidget {
const PageB2({Key? key}) : super(key: key);
static Route<void> route(Todo todo) {
return MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => TodoBloc(
todoRepository: context.read<TodosRepository>(),
todo: todo,
),
child: const PageB2(),
),
);
}
#override
Widget build(BuildContext context) {
final todos = context.watch<CollectionBloc>().state.todos;
final todo = todos.firstWhere((element) => element.id == state.todo.id);
return BlocBuilder<TodoBloc, TodoState>(
builder: (context, state) {
return ListView.separated(
itemCount: todo.actions.length,
separatorBuilder: (context, index) => const Divider(height: 8),
itemBuilder: (context, index) {
return ListTile(
title: Text(todo.actions[index].name),
onTap: () {
Navigator.of(context).push(
PageC.route(todo, todo.actions[index]),
);
},
);
},
);
},
);
}
}
I think I don't know how to apply emit.forEach() in this case. XD

flutter_login and flutter_bloc navigation after authentication: BlocListener not listening to state change

I am trying to combine this with bloc, using this design pattern from the docs.
After the state has been instantiated, BlocListener stops listening to the authentication bloc and I am kind of forced to use the login form's onSubmitAnimationCompleted method for routing, which makes the listener useless in the first place.
MaterialApp() is identical to the example provided in the docs (I am trying to navigate from the login screen, which is the initialRoute in this case, to the home screen)
the login form looks like this:
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState> (
listener: (context, state) {
// first time around state is read
if (state is AuthenticationAuthenticated) {
Navigator.of(context).pushNamed(Home.routeName);
}
},
child: BlocBuilder(
bloc: _loginBloc,
builder: (BuildContext context, state) {
return FlutterLogin(
title: 'Login',
logo: const AssetImage('lib/assets/madrid.png'),
onLogin: _authUser,
onSignup: _signupUser,
onRecoverPassword: _recoverPassword,
loginProviders: <LoginProvider>[
... Providers here...
],
// if this method is omitted, I'll get a [ERROR:flutter/lib/ui/ui_dart_state.cc(209)]
onSubmitAnimationCompleted: () {
Navigator.of(context).pushNamed(Home.routeName);
},
);
},
),
);
}
I am splitting events an state between two blocs, 'AuthenticationBloc' (wraps entire app, if a token has been stored then the state will be 'AuthenticationAuthenticated') and 'LoginBloc' (used for login/logout events)
#1 when I click on the sign up button, the associated method will call _loginBloc?.add(SignUpButtonPressed(email: email, password: password))
#2 fast forward to the bloc:
LoginBloc({required this.authenticationBloc, required this.loginRepository})
: super(const SignInInitial()) {
on<SignUpButtonPressed>(_signUp);
}
...
FutureOr<void> _signUp<LoginEvent>(SignUpButtonPressed event, Emitter<LoginState> emit) async {
emit(const SignInLoading());
try {
final credentials = User(email: event.email, password: event.password);
final success = await loginRepository.signUp(credentials);
if (success) {
final token = await loginRepository.signIn(credentials);
authenticationBloc.add(LoggedIn(email: event.email, token: token));
} else {
emit(const SignInFailure(error: 'Something went wrong'));
}
} on Exception {
emit(const SignInFailure(error: 'A network Exception was thrown'));
} catch (error) {
emit(SignInFailure(error: error.toString()));
}
}
this is successful, and it triggers the authentication bloc:
AuthenticationBloc({required this.userRepository})
: super(const AuthenticationUninitialized()) {
on<LoggedIn>(_loggedIn);
}
...
FutureOr<void> _loggedIn<AuthenticationEvent>(LoggedIn event, Emitter<AuthenticationState> emit) async {
await userRepository?.persistEmailAndToken(
event.email, event.token);
await _initStartup(emit);
}
...
Future<void> _initStartup(Emitter<AuthenticationState> emit) async {
final hasToken = await userRepository?.hasToken();
if (hasToken != null && hasToken == true) {
emit(const AuthenticationAuthenticated());
return;
} else {
emit(const AuthenticationUnauthenticated());
}
}
... and at the end of this, the state is updated to AuthenticationAuthenticated, which is the expected behaviour, and the observer logs the transition as expected.
Now, this state change should trigger the navigation from within the BlocListener, but nope.
I would like to get rid of the Navigator inside the onSubmitAnimationCompleted, and rely on the state change.
I reckon this might be caused by Equatable, as my state extends that:
abstract class AuthenticationState extends Equatable {
const AuthenticationState();
#override
List<Object> get props => [];
}
class AuthenticationAuthenticated extends AuthenticationState {
const AuthenticationAuthenticated();
}
However, I've tried for hours, but I can't find anything in the docs, github, or SO that works.
So, I have not been able to get rid of the Navigator inside of onSubmitAnimationCompleted (I guess the BlocListener is disposed when the form is submitted, and before the animation is completed), but in the process I've managed to make my state management clean and robust, so I'll leave a little cheatsheet below, feel free to comment or give your opinion:
Assuming your widget's build method looks something like this:
#override
Widget build(BuildContext context) {
return BlocListener<AuthenticationBloc, AuthenticationState> (
bloc: _authenticationBloc,
listener: (context, state) {
if (state.status == AuthenticationAppState.authenticated) {
Navigator.of(context).pushNamed(Home.routeName);
}
},
child: BlocBuilder(
bloc: _loginBloc,
builder: (BuildContext context, state) {
return FlutterLogin(
...
and that your events extend Equatable
import 'package:equatable/equatable.dart';
abstract class AuthenticationEvent extends Equatable {
const AuthenticationEvent();
#override
List<Object> get props => [];
}
class LoggedIn extends AuthenticationEvent {
final String email;
final dynamic token;
const LoggedIn({ required this.email, this.token });
#override
List<Object> get props => [email, token];
}
your Bloc will look like:
class AuthenticationBloc extends Bloc<AuthenticationEvent, AuthenticationState> {
final SecureStorage? userRepository;
AuthenticationBloc({required this.userRepository})
: super(const AuthenticationState.uninitialized()) {
on<LoggedIn>(_loggedIn);
on<LoggedOut>(_loggedOut);
on<UserDeleted>(_userDeleted);
}
...
FutureOr<void> _loggedOut<AuthenticationEvent>(LoggedOut event, Emitter<AuthenticationState> emit) async {
emit(const AuthenticationState.loggingOut());
await userRepository?.deleteToken();
// API calls here
// event has access the event's properties e.g. event.email etc
}
the state has been refactored to:
import 'package:equatable/equatable.dart';
enum AuthenticationAppState {
uninitialized,
unauthenticated,
authenticated,
loggingOut,
loading,
}
class AuthenticationState extends Equatable {
const AuthenticationState._({
required this.status,
});
const AuthenticationState.uninitialized() : this._(status: AuthenticationAppState.uninitialized);
const AuthenticationState.unauthenticated() : this._(status: AuthenticationAppState.unauthenticated);
const AuthenticationState.authenticated() : this._(status: AuthenticationAppState.authenticated);
const AuthenticationState.loggingOut() : this._(status: AuthenticationAppState.loggingOut);
const AuthenticationState.loading() : this._(status: AuthenticationAppState.loading);
final AuthenticationAppState status;
#override
List<Object> get props => [status];
}

Should I use final in models with equatable and flutter_bloc to distinguish 2 states?

I'm creating an app where you login and go to a page where you have a list of your restaurants, you have also a form where you can add a new restaurant.
This part works.
The problem is that when i click add the restaurant is added in firestore correctly, but the list doesn't refresh. I usually yield 2 states, a LoadingState and a LoadedRestaurantsListState, but with the last version of flutter_bloc this trick doesn't work, seems like just the last state yielded is received, but the previous was LoadedRestaurantsListState, so they are equals and the blocbuilder ignores the second one. So I've to use the equatable's props to distinguish the 2 states, but in the equatable documentation is written: "Note: Equatable is designed to only work with immutable objects so all member variables must be final".
So I've to make all the model's fields final, but if I do it how can i modify just one o two fields when I need it to?
What is the best practice?
If someone has examples, or videos, etc it would be very appreciated.
Thanks in advance
Without props
FirebaseBloc.dart
Stream<FirebaseState> mapEventToState(
FirebaseEvent event,
) async* {
print("event firebase ${event.runtimeType.toString()}");
if (event is CreateRestaurantFirebaseEvent) {
yield LoadingState();
await _databaseService.createRestaurant(event.restaurant, event.user);
List<Restaurant> restaurantsList = await _databaseService
.loadRestaurantsList(event.user.restaurantsIDsList);
yield LoadedRestaurantsListState(restaurantsList);
}
if (event is LoadRestaurantsListEvent) {
List<Restaurant> restaurantsList =
await _databaseService.loadRestaurantsList(event.restaurantsIDs);
yield LoadedRestaurantsListState(restaurantsList);
}
FirebaseState.dart
class LoadingState extends FirebaseState {
#override
List<Object> get props => [];
}
class LoadedRestaurantsListState extends FirebaseState {
List<Restaurant> restaurantsList;
LoadedRestaurantsListState(this.restaurantsList);
#override
List<Object> get props => [];
}
view.dart
class RestaurantSelectionScreen extends StatefulWidget {
final User user;
RestaurantSelectionScreen({
#required this.user,
});
#override
_RestaurantSelectionScreenState createState() =>
_RestaurantSelectionScreenState();
}
class _RestaurantSelectionScreenState extends State<RestaurantSelectionScreen> {
FirebaseBloc _firebaseBloc;
#override
void initState() {
super.initState();
_firebaseBloc = FirebaseBloc();
_firebaseBloc.add(LoadRestaurantsListEvent(widget.user.restaurantsIDsList));
}
#override
Widget build(BuildContext context) {
return BlocProvider<FirebaseBloc>(
create: (context) => _firebaseBloc,
child: Scaffold(
body: SingleChildScrollView(
child: Center(
child: BlocBuilder(
cubit: _firebaseBloc,
builder: (context, state) {
print("state ${state.runtimeType.toString()}");
if (state is InitialFirebaseState) {
return CircularProgressIndicator();
} else if (state is LoadedRestaurantsListState) {
return buildUI(state);
} else if (state is LoadingState) {
return CircularProgressIndicator();
} else {
return _CreateRestaurantFormWidget(widget.user);
}
},
),
),
),
),
);
}