How to update screen A when navigating back from screen B with bloc flutter - flutter

I have a screen(A) which use 'AccountFetched' state to load list of data from database:
return BlocProvider<AccountBloc>(
create: (context) {
return _accountBloc..add(FetchAccountEvent());
},
child: BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is AccountFetched) {
accounts = state.accounts;
}
And in my second screen(B) I call AddAccountEvent to add data to database and navigate back to screen(A) which is the parent screen.
onPressed: () {
BlocProvider.of<AccountBloc>(context)
..add(AddAccountEvent(account: account));
Navigator.of(context).pop();
}
But when I navigate back to screen A, the list of data is not updated.
Should I refresh the screen(A) manually or how can I update the state of bloc?
This is my bloc class:
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc() : super(AccountInitial());
#override
Stream<AccountState> mapEventToState(AccountEvent event) async* {
if (event is FetchAccountEvent) yield* _fetchAccountEvent(event, state);
if (event is AddAccountEvent) yield* _addAccountEvent(event, state);
}
Stream<AccountState> _fetchAccountEvent(
FetchAccountEvent event, AccountState state) async* {
yield FetchingAccount();
final dbAccount = await DBAccountRepository.instance;
List<Account> accounts = await dbAccount.accounts();
yield AccountFetched(accounts: accounts);
}
Stream<AccountState> _addAccountEvent(
AddAccountEvent event, AccountState state) async* {
yield AddingAccount();
final dbAccount = await DBAccountRepository.instance;
final insertedId = await dbAccount.insertAccount(event.account);
yield AccountAdded(insertedId: insertedId);
}
}

I was experiencing the same problem, the solution I found was to make screen A wait for screen B to close, and after that trigger the data reload event for screen A.
onTap: () async {
await Navigator.of(context).push(ScreenB());
_bloc.add(RequestDataEvent());
}
So it will only execute the event after screen B is closed

well, you are creating a new bloc on page A. try to move BlocProvider 1 level up. so when the build function of page A calls, doesn't create a new bloc.

You should have a BlocListener in screen A listening for AccountState , with a if ( state is AccountFetched ) {} statement and perform your data update in a setState() . Depending on how long it takes to display screen A you might have the AccountFetched state been yielded before so screen A won't see it, in this case you could delay by 250 milliseconds yielding it in your _addAccount method either with a Timer or a Future.delayed.
Hope it helped.

Related

Flutter bloc snackbar and yielding state twice

How to achieve showing snackbar without losing bloc state?
Suppose i have this widget class
Scaffold(
body: LoadingOverlay(
child: Container(
child: BlocConsumer<PostingBloc, PostingState>(
bloc: newBloc,
listener: (context, state) async {
if (state is PostingSuccess) {
//Show Snackbar
}
builder : (context, state){return Container(child:Text(state.text))}
}
then the bloc state is like this
class PostingState extends Equatable{
String text;
}
class PostingSuccess extends JobPostingState{
}
and the bloc mapEventToState is like this
PostingStateget initialState => PostingState();
Stream<PostingState> mapEventToState(
PostingEvent event,
) async* {
if (event is SuccessPost) {
yield PostingSuccess();
yield state;
}
Is it the correct way to implement snackbar on the bloc state management do i really need to yield twice to make the text still shown without the need to adding new variable like boolean showSuccess etc on my PostingState class.
Or is it the wrong way since there will be a time when the text is gone which is when the Posting Success emitted? I am quite confused on how to do this on bloc, any suggestion is really appreciated. thankyou
You can add your String inside your event and your state. After that:
if (event is SuccessPost) {
yield PostingSuccess(text: event.text);
}

Previous screen Bloc call after open new screen in Flutter

I am using Bloc pattern in my project, But my previous screen's Bloc still call in my new screen. I have dispose my Bloc but in screen dispose method is not call. Please help to solve this problem.
Bloc file:
class ForgotPasswordBloc extends Object {
final PublishSubject<ForgotPassState> _passStateSubject = new PublishSubject();
Stream<ForgotPassState> get passStateStream => _passStateSubject.stream;
void changeState({ForgotPassState state}) => _passStateSubject.sink.add(state);
forgotPasswordSubmit(String email) async {
changeState(state: ForgotPassState(status: ForgotPassStatus.CALLING, message: "calling"));
var response = await post(Constant.API_URL+"forgotPassword", body: {
"email": email,
});
ForgotPasswordModal data = ForgotPasswordModal.fromJson(json.decode(response.body));
if (data.status == "success") {
await changeState(state: ForgotPassState(status: ForgotPassStatus.SUCCESS, message: "Success"));
} else {
changeState(
state: ForgotPassState(
status: ForgotPassStatus.ERROR, message: data.message));
}
}
dispose() {
print("dispose called==========>");
_passStateSubject.close();
}
}
Here I call the bloc method:
StreamBuilder<ForgotPassState>(
stream: forgotpass_bloc.passStateStream,
builder: (context, AsyncSnapshot<ForgotPassState> snapshot) {
switch (snapshot.data.status) {
case ForgotPassStatus.SUCCESS:
WidgetsBinding.instance.addPostFrameCallback((_) => showInformationDialog(context,email_controller.text.trim()));
break;
default:
return Container();
}
},
),
In above code after getting success response my information dialog is open, In dialog I have close button After click on the close button I have open new Screen but in that screen my previous screen dialog is display.
I guess you need to dispose the bloc in the ui widget, where you generate the bloc instance and not in the bloc class itself

Flutter Bloc Library

In an application I am trying to implement the cart functionality. ItemDetailsScreen has addItemBtn, which returns null after the item is added. This function works fine, but the problem is that when I go to cartScreen and clear the cart, and then go back to ItemDetailsScreen, addItemBtn still returns null. To return the add state, I must use a hot reload. It looks like the state is not updating !? So how to solve this?
addItemBtn:
BlocBuilder<CartFunctionsCubit, CartFunctionsState>(
builder: (context, state) {
return state.map(
initial: (_) => Container(),
cartLoaded: (state) => FlatButton(
onPressed: state.userCart.items.contains(item)
? null
: () {
context.read<CartFunctionsCubit>().addToCart(item);
context.read<CartFunctionsCubit>().startApp();
},
child: state.userCart.items.contains(item)
? Text('Added')
: Text('Add'),
),
);
},
);
Cubit:
Future<void> startApp() async {
final userCart = await cartFacade.getUserCart();
emit(CartFunctionsState.cartLoaded(userCart: userCart));
}
Future<void> addToCart(Item item) async {
cartFacade.addToCart(item);
}
Navigate to cart screen I am using
Navigator.of(context).pushNamed('/cart');
you can await push function and then call the context.read<CartFunctionsCubit>().startApp();
await Navigator.of(context).pushNamed('/cart');
context.read<CartFunctionsCubit>().startApp();
It will refresh the data when you came back from cart page
And if you want change the data when you change something in cart page. give boolean in Navigator.pop()
In Cart Page
bool needToRefresh = false/// when there is a change set needToRefresh = true
Navigator.pop(context,needToRefresh );/// and pass the value here
In Item Details Screen
bool needToRefresh = await Navigator.of(context).pushNamed('/cart');
if(needToRefresh !=null && needToRefresh)
context.read<CartFunctionsCubit>().startApp();
It will refresh the data only when needToRefresh is true;

BlocBuilder not updating after cubit emit

UPDATE After finding the onChange override method it appears that the updated state is not being emitted #confused
UPDATE 2 Further debugging revealed that the StreamController appears to be closed when the updated state attempts to emit.
For some reason 1 of my BlocBuilders in my app refuses to redraw after a Cubit emit and for the life of me I cannot figure out why, there are no errors when running or debugging, the state data is being updated and passed into the emit.
Currently, it is a hook widget, but I have tried making it Stateless, Stateful, calling the cubit method in an effect, on context.bloc.
I have tried making it a consumer, nothing makes it into the listen, even tried messing with listenWhen and buildWhen and nothing is giving any indication as to why this is not building.
It renders the loading state and that is where it ends.
Widget:
class LandingView extends HookWidget {
#override
Widget build(BuildContext context) {
final hasError = useState<bool>(false);
return Scaffold(
key: const Key(LANDING_VIEW_KEY),
body: BlocProvider<CoreCubit>(
create: (_) => sl<CoreCubit>()..fetchDomainOptions(),
child: BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
switch (state.status) {
case CoreStatus.loaded:
return LandingViewLoaded();
case CoreStatus.error:
return LandingViewError();
case CoreStatus.loading:
default:
return AppIcon();
}
},
),
),
);
}
}
Cubit method:
Future<void> fetchDomainOptions() async {
final inputEither = await getDomainOptions();
return inputEither.fold(
_handleFailure,
(options) {
emit(state.copyWith(
domainOptions: options,
status: CoreStatus.loaded,
));
},
);
}
I have a few other widgets that work off freezed data classes and work of the same status key logic without any issues, I even went as far as trying to add a lastUpdated timestamp key onto it to make even more data change, but in the initial state domainOptions is null and status is CoreStatus.loading, which should already be enough to trigger a UI update.
TIA
I haven't figured out yet why exactly this happens but I believe we're dealing with some kind of race condition here.
Also, I don't know the proper solution to work around that but I have found that delaying calling emit() for a short amount of time prevents this issue from occurring.
void load() async {
try {
emit(LoadingState());
final vats = await provider.all();
await Future<void>.delayed(const Duration(milliseconds: 50));
emit(LoadedState(vats));
} catch (e) {
print(e.toString());
emit(ErrorState());
}
}
Add await Future<void>.delayed(const Duration(milliseconds: [whatever is needed])); before emitting your new state.
I came to this conclusion after reading about this issue on cubit's GitHub: Emitted state not received by CubitBuilder
i resolved my problem, i was using getIt.
i aded the parameter bloc.
my problem was not recibing the events (emit) of the cubit (bloc)
child: BlocBuilder<CoreCubit, CoreState>(
bloc: getIt<CoreCubit>(), // <- this
builder: (context, state) {
},
),

Flutter BLoC mapEventToState gets called only the first time for an event and not called each next time that event is fired

I have Courses and Tasks. Each Course has many Tasks. That is why I am using different screens in the app to show a list of courses and after a tap on a course, I am navigating to the next screen - a list of tasks. Here is my onTap method of the list of courses:
onTap: () {
TasksPageLoadedEvent pageLoadedEvent =
TasksPageLoadedEvent(
courseId: state.courses[index].id,
truckNumber: this.truckNumber,
);
serviceLocator<TaskBloc>().add(pageLoadedEvent);
Routes.sailor(
Routes.taskScreen,
params: {
Routes.courseNumber:
state.courses[index].courseNumber,
Routes.truckNumber: this.truckNumber,
Routes.courseId: state.courses[index].id,
},
);
}
I create a TasksPageLoadedEvent, pass it to the TaskBloc and navigate to the Tasks page.
Here is the TaskBloc and how it handles the mapping Event - State:
#override
Stream<TaskState> mapEventToState(
TaskEvent event,
) async* {
if (event is TasksLoadingEvent) {
yield TasksLoadingState();
} else if (event is TasksReloadingErrorEvent) {
yield TasksErrorState();
} else if (event is TasksFetchedFailureEvent) {
yield TaskFetchedStateFailureState(error: event.failure);
} else if (event is TasksPulledFromServerEvent) {
yield TasksPulledFromServerState(
truckNumber: event.truckNumber,
courseNumber: event.courseNumber,
courseId: event.courseId,
);
} else if (event is TasksPageLoadedEvent) {
yield TasksLoadingState();
final networkInfoEither = await this.getNetworkInfoQuery(NoQueryParams());
yield* networkInfoEither.fold((failure) async* {
yield TasksErrorState();
}, (success) async* {
if (success) {
final getTasksEither = await getTasksQuery(
GetTasksParams(
truckNumber: event.truckNumber,
courseId: event.courseId,
),
);
yield* getTasksEither.fold((failure) async* {
yield TaskFetchedStateFailureState(error: "coursesDatabaseError");
}, (result) async* {
if (result != null) {
yield TasksFetchedState(tasks: result);
} else {
yield TaskFetchedStateFailureState(
error: "coursesFetchFromDatabaseError");
}
});
} else {
yield TasksNoInternetState();
}
});
}
}
When I get navigated to the Tasks page, the BlocBuilder checks the state and handles the building accordingly. I have a Go Back functionality that navigates back to the Courses page:
onPressed: () {
serviceLocator<CourseBloc>().add(
CoursesPageLoadedEvent(truckNumber: this.truckNumber),
);
Navigator.of(context).pop(true);
},
This fires the similar event for the previous page and it gets re-loaded.
The problem I am facing happens if I want to go to another course and see its tasks. If I tap on another item in the list and therefore fire a new TasksPageLoadedEvent (with new properties) the mapEventToState() doesn't get called at all.
I have had similar issues with BLoC before, but they were regarding the BlocListener and states extending Equatable. That is why I had my events NOT extending Equatable (although I am not sure whether this was the issue here). But still nothing happens.
Here are my Events:
abstract class TaskEvent {
const TaskEvent();
}
class TasksPageLoadedEvent extends TaskEvent {
final String truckNumber;
final int courseId;
TasksPageLoadedEvent({
this.truckNumber,
this.courseId,
});
}
class TasksFetchedFailureEvent extends TaskEvent {
final String failure;
TasksFetchedFailureEvent({
this.failure,
});
}
class TasksLoadingEvent extends TaskEvent {}
class TasksReloadingErrorEvent extends TaskEvent {}
class TasksPulledFromServerEvent extends TaskEvent {
final String courseNumber;
final String truckNumber;
final int courseId;
TasksPulledFromServerEvent({
#required this.courseNumber,
#required this.truckNumber,
#required this.courseId,
});
}
How should I handle my back-and-forth between the two pages using two BLoCs for each page?
OK, I found an answer myself!
The problem, of course, as Federick Jonathan implied - the instance of the bloc. I am using a singleton instance created by the flutter package get_it. Which is really useful if you are implementing dependency injection (for a clean architecture for example).
So the one instance was the problem.
Luckily the package has implemented the neat method resetLazySingleton<T>.
Calling it upon going back resets the bloc used in that widget. Therefore when I navigate again to the Tasks page I am working with the same but reset instance of that bloc.
Future<bool> _onWillPop() async {
serviceLocator.resetLazySingleton<TaskBloc>(
instance: serviceLocator<TaskBloc>(),
);
return true;
}
I hope this answer would help someone in trouble with singletons, dependency injections and going back and forth within a flutter app with bloc.
for anyone else who has similar issue:
in case you are listening to a repository stream and looping through emitted object, it cause mapEventToState gets blocked. because the loop never ends.
Stream<LoaderState<Failure, ViewModel>> mapEventToState(
LoaderEvent event) async* {
yield* event.when(load: () async* {
yield const LoaderState.loadInProgress();
await for (final Either<Failure, Entity> failureOrItems in repository.getAll()) {
yield failureOrItems.fold((l) => LoaderState.loadFailure(l),
(r) => LoaderState.loadSuccess(mapToViewModel(r)));
}
});
}
what you should do instead of await for the stream, listen to stream and then raise another event, and then process the event:
watchAllStarted: (e) async* {
yield const NoteWatcherState.loadInProgress();
_noteStreamSubscription = _noteRepository.watchAll().listen(
(failureOrNotes) =>
add(NoteWatcherEvent.notesReceived(failureOrNotes)));
},
notesReceived: (e) async* {
yield e.failureOrNotes.fold(
(failure) => NoteWatcherState.loadFailure(failure),
(right) => NoteWatcherState.loadSuccess(right));
},