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
Related
I have an bloc that receives an event called OpenTourStop that invokes a function whose first line of code invokes emit(Waiting()); and then proceeds to execute some logic before emitting a different state. In the UI, the BlocConsumer is supposed to print out a message to the console as soon as state equals Waiting, but it NEVER does. The bloc does not emit the Waiting state, but does emit the other states that result from completing the function. What am I doing wrong?
Below are the relevant sections of code for the bloc and UI
Bloc code:
class QuiztourManagerBloc
extends Bloc<QuiztourManagerEvent, QuiztourManagerState> {
final QuiztourRemoteData _repo;
QuiztourManagerBloc({QuiztourRemoteData repo})
: _repo = repo,
super(QuiztourManagerInitial()) {
on<OpenTourStop>(_openTourStop);
}
_openTourStop(event, emit) {
emit(Waiting()); // Why doesn't the Waiting state show up in the UI?
final _tourStopIndex = event.tourStopIndex;
// section of code removed for clarity
if (_quizPlayDoc.seenRules && tourStopGameResults.isEmpty) {
emit(ShowQuizQuestionViewManager(
quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
// emit(ShowQuizQuestions(quizPlayDoc: _quizPlayDoc, tourStopIndex: _tourStopIndex));
} else if (tourStopGameResults.length > 0) {
emit(ShowQuizTourStopScreen(
tour: event.tour,
tourStopIndex: event.tourStopIndex,
quizPlayDoc: _quizPlayDoc,
maxTourStopPoints: _maxTourStopPoints.toString(),
pointsEarned: _tourStopScore.toString(),
));
} else {
emit(ShowQuizRules(_quizPlayDoc));
}
}
}
UI code (from class QuizTourStopViewManager) :
#override
Widget build(BuildContext context) {
return BlocConsumer<QuiztourManagerBloc, QuiztourManagerState>(
builder: (context, state) {
if (state is Waiting) {
print('!!!!!!!!!!!!!!!!!!!!! Waiting '); // Why does this line never get executed?
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
else if (state is ShowQuizTourStopScreen) {
return QuizTourStop( );
}
},
listener: (_, state) {},
);
}
The UI that triggers the event is a button. The code associated with that button is below:
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
if (tourType == "quizTour") {
BlocProvider.of<QuiztourManagerBloc>(context)
.add(OpenTourStop(
tour: tour,
tourStopIndex: selectedTourStopIndex,
));
return QuizTourStopViewManager(
tour: tour,
// game: widget.game,
selectedTourStopIndex: selectedTourStopIndex,
);
I guess that when you send 'OpenTourStop' event at 'onTap' method, 'QuizTourStopViewManager' page is not builded.
So would you try to change event call position after 'OpenTourStop' page is builded?
At initState() method inside.
or
At 'bloc' parameter in BlocConsumer method.
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.
I am trying to log in with Google. When googleSignInAccount I yield new state with PlatformExecption. Now, How can I access that value in order to do some changes.
try{
final GoogleSignInAccount googleSignInAccount = await googleSignIn.signIn();
if(googleSignInAccount == null){
yield GoogleLoginErrorState(error: PlatformException(code: 'user-cancel'));
}
}
my state.dart
class GoogleLoginErrorState extends GoogleLoginState {
final PlatformException error;
GoogleLoginErrorState({this.error});
#override
List<Object> get props => [error];
}
my BlocBuilder
if (state == GoogleLoginErrorState()) {
}
For side effects such as showing snackbars / dialogs or navigating to another screen, you have to use BlocListener, something like this:
BlocListener<YourGoogleSigninBloc, YourGoogleSigninState>(
listener: (context, state) {
if(state is GoogleLoginErrorState){
// show snackbar here
}
},
child: YourWidget(),
)
you can also use BlocConsumer instead of nesting BlocListeners and BlocBuilders like this:
BlocConsumer<YourGoogleSigninBloc, YourGoogleSigninState>(
listener: (context, state) {
if(state is GoogleLoginErrorState){
// show snackbar here
}
},
builder: (context, state) {
return YourWidget();
},
)
In the bloc builder,
if (state is GoogleLoginErrorState) {
}
This checks if the state data type is GoogleLoginErrorState
And use the state
Text(state.error.code)
Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies.
It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.
So, I have a cubit with a function that POSTs some data. I have three states, DataLoading, DataError and DataLoaded for my cubit.
When the user taps on a button, I call the function in the cubit. After that, I have a BlocListener to wait until the Cubit emits the DataLoaded state. The issue is that the listener is reacting to the state changes.
Button(
text: 'Add',
onTap: () {
final data = _textController.text;
context.read<PostDataCubit>().post(data);
BlocListener<PostDataCubit, PostDataState>(
listener: (context, state) {
if (state is DataLoaded) {
// navigate to another route
} else if (state is DataError) {
// show error
}
},
);
}
),
I've tried using await on the read() call but that didn't work. How do I react to the state changes here? Thanks.
This BlocListener isn't listening because you have added the listener inside a function instead of adding it in widget tree. Wrap your button inside BlocConsumer widget and it will works fine. Have a look into below code.
BlocListener<PostDataCubit, PostDataState>(
listener: (context, state) {
if (state is DataLoaded) {
// navigate to another route
} else if (state is DataError) {
// show error
}
},
builder: (context, state) {
return Button(
text: 'Add',
onTap: () {
final data = _textController.text;
context.read<PostDataCubit>().post(data);
});
},
),